On my brand new Windows 11 desktop, the bitmap I've been using to indicate a 'connected database' state in my stable production app is suddenly looking terrible on my 4K 150% scaled display (the monitor is still the same as before). The issue seems specific to TreeView
because the same bitmap on the same Form
looks OK when set as the image for a Label
for example. It also still looks OK on a Win 10 VM running on the new machine. And it mainly affects the green one. Weird.
Anyway, I can't just sit and cry about it - I really do need to come up with a new way of drawing this that looks right 100% of the time. So I'm trying a new approach using a glyph font and it looks nice and clear when I put it up on a set of labels.
Looking good in the TableLayoutPanel.
What I need to do now is generate an ImageList to use for the tree view, and as a proof of concept I tried using Control.DrawToBitmap
to generate a runtime ImageList from the labels. I added a #DEBUG
block that saves the bitmaps and I can open them up in MS Paint and they look fine (here greatly magnified of course).
Looking good in the .bmp files.
And for sure this improves things, but there are still some obvious pixel defects that look like noisy anti-aliasing or resizing artifacts, even though I'm taking care to use consistent 32 x 32 sizes for everything. I've messed with the ColorDepth
ans ImageSize
properties of the ImageList
. I've wasted hours trying to understand and fix it. It's happening in my production code. It's happening in the minimal reproducible sample I have detailed below. So, before I tear the rest of my hair out, maybe someone can spot what I'm doing wrong, or show me a better way.
Here's my code or browse full sample on GitHub.
public partial class HostForm : Form
{
public HostForm()
{
InitializeComponent();
#if DEBUG
_imgFolder = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Paint")!;
Directory.CreateDirectory(_imgFolder);
#endif
}
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
BackColor = Color.Teal;
var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Fonts", "database.ttf")!;
privateFontCollection.AddFontFile(path);
var fontFamily = privateFontCollection.Families[0];
var font = new Font(fontFamily, 13.5F);
var backColor = Color.FromArgb(128, Color.Teal);
tableLayoutPanel.BackColor = backColor;
// Stage the glyphs in the TableLayoutPanel.
setLabelAttributes(label: label0, font: font, text: "\uE800", foreColor: Color.LightGray, backColor: backColor);
setLabelAttributes(label: label1, font: font, text: "\uE800", foreColor: Color.LightSalmon, backColor: backColor);
setLabelAttributes(label: label2, font: font, text: "\uE800", foreColor: Color.LightGreen, backColor: backColor);
setLabelAttributes(label: label3, font: font, text: "\uE800", foreColor: Color.Blue, backColor: backColor);
setLabelAttributes(label: label4, font: font, text: "\uE800", foreColor: Color.Gold, backColor: backColor);
setLabelAttributes(label: label5, font: font, text: "\uE801", foreColor: Color.LightGray, backColor: backColor);
setLabelAttributes(label: label6, font: font, text: "\uE801", foreColor: Color.LightSalmon, backColor: backColor);
setLabelAttributes(label: label7, font: font, text: "\uE801", foreColor: Color.LightGreen, backColor: backColor);
setLabelAttributes(label: label8, font: font, text: "\uE801", foreColor: Color.Blue, backColor: backColor);
setLabelAttributes(label: label9, font: font, text: "\uE801", foreColor: Color.Gold, backColor: backColor);
setLabelAttributes(label: label10, font: font, text: "\uE803", foreColor: Color.LightGray, backColor: backColor);
setLabelAttributes(label: label11, font: font, text: "\uE803", foreColor: Color.LightSalmon, backColor: backColor);
setLabelAttributes(label: label12, font: font, text: "\uE803", foreColor: Color.LightGreen, backColor: backColor);
setLabelAttributes(label: label13, font: font, text: "\uE803", foreColor: Color.Blue, backColor: backColor);
setLabelAttributes(label: label14, font: font, text: "\uE802", foreColor: Color.LightGray, backColor: backColor);
setLabelAttributes(label: label15, font: font, text: "\uE804", foreColor: Color.LightGreen, backColor: backColor);
makeRuntimeImageList();
}
private void setLabelAttributes(Label label, Font font, string text, Color foreColor, Color backColor)
{
label.UseCompatibleTextRendering = true;
label.Font = font;
label.Text = text;
label.ForeColor = foreColor;
label.BackColor = Color.FromArgb(200, backColor);
}
private void makeRuntimeImageList()
{
var imageList22 = new ImageList(this.components);
imageList22.ImageSize = new Size(32, 32);
imageList22.ColorDepth = ColorDepth.Depth8Bit;
foreach (
var label in
tableLayoutPanel.Controls
.Cast<Control>()
.Where(_=>_ is Label)
.OrderBy(_=>int.Parse(_.Name.Replace("label", string.Empty))))
{
Bitmap bitmap = new Bitmap(label.Width, label.Height);
label.DrawToBitmap(bitmap, label.ClientRectangle);
imageList22.Images.Add(bitmap);
#if DEBUG
bitmap.Save(Path.Combine(_imgFolder, $"{label.Name}.{ImageFormat.Bmp}"), ImageFormat.Bmp);
#endif
}
this.treeView.StateImageList = imageList22;
}
#if DEBUG
readonly string _imgFolder;
#endif
PrivateFontCollection privateFontCollection = new PrivateFontCollection();
}
If you want to use the TreeView.StateImageList
rather than the TreeView.ImageList
, then you need to have/create 16x16 images. Setting the size of the ImageSize
property to larger or smaller sizes does nothing but outputs
blurred, distorted overlapping pixels images. Because with TreeView.StateImageList
, the non-16x16 images will be resized to fit the allocated spaces, the bounds of the CheckBoxes since the common use of the TreeView.StateImageList
is to use the first (unchecked) and second (checked) images of the list to indicate the nodes checked state of a TreeView
with CheckBoxes
property is set to true
.
From the docs
The state images displayed in the TreeView are 16 x 16 pixels by default. Setting the ImageSize property of the StateImageList will have no effect on how the images are displayed. However, the state images are resized according to the system DPI setting when the app.config file contains the following entry:
<appSettings>
<add key="EnableWindowsFormsHighDpiAutoResizing" value="true" />
</appSettings>
... and
When the CheckBoxes property of a TreeView is set to true and the StateImageList property is set, each TreeNode that is contained in the TreeView displays the first and second images from the StateImageList to indicate an unchecked or checked state, respectively.
Taking that into account to convert font glyphs to images will result acceptable quality. Make sure to choose a proper font size to sharpen the glyph's details.
Example
Here's a simple helper class to convert some of the Segoe MDL2 Assets
font glyphs to images.
public static class SegoeMDL2AssetsFont
{
public enum Glyph
{
Video = 0xE714,
Search = 0xE721,
FavoriteStar = 0xE734,
FavoriteStarFill = 0xE735,
GripperTool = 0xE75E,
ContactPresence = 0xE8CF,
Like = 0xE8E1,
FeedbackApp = 0xE939,
Robot = 0xE99A
}
public static Bitmap CreateGlyphImage(
Glyph glyph,
Size imgSize,
float fontSize,
FontStyle fontStyle,
Color foreColor,
Color backColor)
{
var bmp = new Bitmap(imgSize.Width, imgSize.Height);
var str = ((char)(int)glyph).ToString();
var rec = new Rectangle(Point.Empty, imgSize);
using (var g = Graphics.FromImage(bmp))
using (var fnt = GetFont(fontSize, fontStyle))
using (var sf = new StringFormat(StringFormat.GenericTypographic))
using (var br = new SolidBrush(foreColor))
{
sf.Alignment = sf.LineAlignment = StringAlignment.Center;
g.Clear(backColor);
g.TextRenderingHint = TextRenderingHint.AntiAliasGridFit;
g.DrawString(str, fnt, br, rec, sf);
}
return bmp;
}
public static string FontName => "Segoe MDL2 Assets";
public static Font GetFont(float fontSize, FontStyle style) =>
new Font(FontName, fontSize, style);
}
... and the implementation.
private void SomeCaller()
{
imgList.Images.Clear();
imgList.ColorDepth = ColorDepth.Depth32Bit;
imgList.ImageSize = new Size(16, 16);
imgList.TransparentColor = Color.Teal;
var sz = imgList.ImageSize;
var fs = 12f;
var style = FontStyle.Regular;
var bkColor = imgList.TransparentColor;
imgList.Images.Add(SegoeMDL2AssetsFont.CreateGlyphImage(
SegoeMDL2AssetsFont.Glyph.FavoriteStar,
sz, fs, style, Color.LightGray, bkColor));
imgList.Images.Add(SegoeMDL2AssetsFont.CreateGlyphImage(
SegoeMDL2AssetsFont.Glyph.FavoriteStarFill,
sz, fs, style, Color.LightSalmon, bkColor));
imgList.Images.Add(SegoeMDL2AssetsFont.CreateGlyphImage(
SegoeMDL2AssetsFont.Glyph.GripperTool,
sz, fs, style, Color.LightGreen, bkColor));
imgList.Images.Add(SegoeMDL2AssetsFont.CreateGlyphImage(
SegoeMDL2AssetsFont.Glyph.Search,
sz, fs, style, Color.Blue, bkColor));
imgList.Images.Add(SegoeMDL2AssetsFont.CreateGlyphImage(
SegoeMDL2AssetsFont.Glyph.Video,
sz, fs, style, Color.Gold, bkColor));
imgList.Images.Add(SegoeMDL2AssetsFont.CreateGlyphImage(
SegoeMDL2AssetsFont.Glyph.ContactPresence,
sz, fs, style, Color.DarkRed, bkColor));
imgList.Images.Add(SegoeMDL2AssetsFont.CreateGlyphImage(
SegoeMDL2AssetsFont.Glyph.Like,
sz, fs, style, Color.Cyan, bkColor));
imgList.Images.Add(SegoeMDL2AssetsFont.CreateGlyphImage(
SegoeMDL2AssetsFont.Glyph.Robot,
sz, fs, style, Color.Maroon, bkColor));
imgList.Images.Add(SegoeMDL2AssetsFont.CreateGlyphImage(
SegoeMDL2AssetsFont.Glyph.FeedbackApp,
sz, fs, style, Color.DarkOrange, bkColor));
treeView1.StateImageList = imgList;
treeView1.BeginUpdate();
treeView1.Nodes.Clear();
for (int i = 0; i < imgList.Images.Count; i++)
{
var tn = new TreeNode($"Node {i + 1}")
{
StateImageIndex = i
};
treeView1.Nodes.Add(tn);
}
treeView1.EndUpdate();
}
... the result