State image shows obvious pixel defects when drawing WinForms TreeView - c#

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

Related

How to compute the correct width of a digit in pixels?

I have a custom control that may have user customizable Font in future (the zoom is already implemented). I must fill a rectangle under two digits that form a base-10 number. I have different colors for zero, one or both of the digits.
With the font {Name = Microsoft Sans Serif Size=16} and the following Graphics.MeasureString method calls:
g.MeasureString("00", Font);
g.MeasureString("0", Font);
I get:
The size of "00" is {Width = 31.5486088 Height = 26.8124962}
The size of "0" is {Width = 19.3298588 Height = 26.8124962}
The width of "0" is a lot bigger that half of the width of "00".
I know of the methods Graphics.MeasureString, it has many overloads, and I also know of the StringFormat class. How can I correctly compute the width of the '0' char?
Because the font will be user-customizable, I do not want to solve the problem using a monospace font.
If I use the following calls:
g.MeasureString("00", Font, 999, StringFormat.GenericTypographic);
g.MeasureString("0", Font, 999, StringFormat.GenericTypographic);
The width of "0" seems to be half of the width of "00", but the digits overlap when drawn with a smaller font size:
Update: In the OnPaint method of an UserControl I have this code:
Graphics g = e.Graphics;
int[] indices = { 0, 1 };
CharacterRange[] charRanges = new CharacterRange[indices.Length];
for (int chx = 0; chx < indices.Length; ++chx)
{
charRanges[chx] = new CharacterRange(indices[chx], 1);
}
StringFormat sf = new StringFormat(StringFormat.GenericDefault);
sf.SetMeasurableCharacterRanges(charRanges);
Region[] regions = e.Graphics.MeasureCharacterRanges("01", Font, e.ClipRectangle, sf);
RectangleF[] r = new RectangleF[regions.Length];
int i = 0;
foreach (Region rr in regions)
{
r[i] = rr.GetBounds(g);
g.DrawRectangle(Pens.Blue, r[i].X, r[i].Y, r[i].Width, r[i].Height);
++i;
}
g.DrawString("0", Font, Brushes.Black, r[0], sf);
g.DrawString("1", Font, Brushes.Black, r[1], sf);
The font is {Name = "Microsoft Sans Serif" Size=25}. When running the program, this is what is visible:
I want to make the digits centered in the blue rectangles. The rectangles must be as big as possible in the UserControl but also leaving space for a percent of the Height of the UserControl. The Font should adapt to the rectangles.
Small adjustments are required to make this work as intended:
TextRenderingHint.ClearTypeGridFit gives a better result when rendering the Text.
It's more precise and works well with the grid-fitting nature of Graphics.DrawString.
See the notes you can find in the answer linked below for more informations on this matter.
StringFormat alignment in both horizontal and vertical dimensions.
A modified method that allows to draw strings of any length.
If the string is larger than the container, it will be wrapped, with the current settings.
Irrelevant: Brush and Pen are declared outside the Paint event, to allow their re-definition when required.
Different implementations of MeasureCharacterRanges here:
How to highlight wrapped text in a control
About Graphics.DrawString and TextRenderingHint.ClearTypeGridFit:
Drawing a Long String on to a Bitmap results in Drawing Issues
Font 48em:
Font 16em:
Font 9em:
Pen pen = new Pen(Color.LightGreen, 1);
Brush brush = new SolidBrush(Color.White);
string sourceDigits = "010011001";
private void panel1_Paint(object sender, PaintEventArgs e)
{
e.Graphics.TextRenderingHint = TextRenderingHint.ClearTypeGridFit;
CharacterRange[] charRanges = new CharacterRange[sourceDigits.Length];
for (int chx = 0; chx < sourceDigits.Length; ++chx) {
charRanges[chx] = new CharacterRange(chx, 1);
}
using (StringFormat sf = new StringFormat())
{
sf.Alignment = StringAlignment.Center;
sf.LineAlignment = StringAlignment.Center;
sf.SetMeasurableCharacterRanges(charRanges);
Region[] regions = e.Graphics.MeasureCharacterRanges(sourceDigits, Font, e.ClipRectangle, sf);
for (int i = 0; i < regions.Length; i++) {
RectangleF rect = regions[i].GetBounds(e.Graphics);
e.Graphics.DrawRectangle(pen, rect.X, rect.Y, rect.Width, rect.Height);
e.Graphics.DrawString(char.ToString(sourceDigits[i]), Font, brush, rect, sf);
}
}
}

How to generate an image based on text with given width and no padding in C#

I want to save a given text as an image. The image should have a fixed width (200px in my example). Around the text there should be no spacing, padding or whatever. Regardless what text is entered, the width should not change, only the height of the text. This works. However, there is still white padding around the text and the text is truncated on the right side.
I have already tried to change StringFormat.GenericTypographic and also tried without AntiAlias, but I do not get it to work. Can anyone help me to get this working?
private void button1_Click(object sender, EventArgs e)
{
Font font = new Font("Arial", 1000, FontStyle.Regular);
Image i = DrawText("TEST MY STRING", font, Color.Red, Color.White);
i.Save("test.jpg", System.Drawing.Imaging.ImageFormat.Jpeg);
}
private Image DrawText(String text, Font font, Color textColor, Color backColor)
{
Image img = new Bitmap(1, 1);
Graphics drawing = Graphics.FromImage(img);
drawing.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAlias;
SizeF sz = drawing.MeasureString(text, font, 0, StringFormat.GenericTypographic);
img.Dispose();
drawing.Dispose();
/* Set maximum width of string. */
int textWidth = 200;
float sf = textWidth / sz.Width;
int textHeight = (int)(sz.Height * sf);
img = new Bitmap(textWidth, textHeight);
drawing = Graphics.FromImage(img);
drawing.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAlias;
drawing.Clear(backColor);
drawing.ScaleTransform(sf, sf);
drawing.DrawString(text, font, Brushes.Black, 0, 0, new StringFormat(StringFormatFlags.NoWrap | StringFormatFlags.NoClip));
drawing.Save();
drawing.Dispose();
return img;
}

C# tabcontrol font error when I set the Alignment to left

I have a problem with the font of the TabControl.
I set the Alignment to Left, and use following code to set the font:
private void tabControl1_DrawItem(Object sender, System.Windows.Forms.DrawItemEventArgs e)
{
Graphics g = e.Graphics;
Brush _textBrush;
// Get the item from the collection.
TabPage _tabPage = tabControl1.TabPages[e.Index];
// Get the real bounds for the tab rectangle.
Rectangle _tabBounds = tabControl1.GetTabRect(e.Index);
if (e.State == DrawItemState.Selected) {
// Draw a different background color, and don't paint a focus rectangle.
_textBrush = new SolidBrush(Color.Black);
g.FillRectangle(Brushes.LightSkyBlue, e.Bounds);
}
else {
_textBrush = new System.Drawing.SolidBrush(e.ForeColor);
e.DrawBackground();
}
// Use our own font.
Font _tabFont = new Font("Calibri", (float)11.0, FontStyle.Regular, GraphicsUnit.Pixel);
// Draw string. Left the text.
StringFormat _stringFlags = new StringFormat();
_stringFlags.Alignment = StringAlignment.Near;
_stringFlags.LineAlignment = StringAlignment.Center;
g.DrawString(_tabPage.Text, _tabFont, _textBrush, _tabBounds, new StringFormat(_stringFlags));
}
I set the font to "Calibri", (float)11.0, but the text in the tabcontrol are smaller. Please refer to the following:
Font Error
What should I do to set the correct font?
Really appreciate your help!
Eric

How can I use a monospace font with PDFsharp?

I am trying to use the "Courier" font with PDFsharp to use its monospace feature, but when I use this font using the library the written text is not monospaced or fixed-width.
This is the code:
// Obtain an XGraphics object to render to
XGraphics gfx = XGraphics.FromPdfPage(page);
// Create a font
double fontHeight = 8;
XFont font = new XFont("Courier", fontHeight, XFontStyle.Regular);
XTextFormatter tf = new XTextFormatter(gfx);
String text = "Hello\r\nMy name is John\r\nMy name, is Peter.";
XRect rect = new XRect(0, 0, 200, 200);
gfx.DrawRectangle(XBrushes.SeaShell, rect);
tf.DrawString(text, font, XBrushes.Black, rect, XStringFormats.TopLeft);
The letter "i" and the character "." break the width.
Use a font that is installed on your computer, for example "Courier New" instead of "Courier".

White bar, and then text watermark?

I would like to apply a watermark to images.
At the moment, I am trying to use this code, but it's failing on different sized images:
public void AddWaterMark(string filePath, string watermarkText)
{
Image img = Image.FromFile(
MapPath(GlobalVariables.UploadPath + "/" + filePath));
Graphics gr = Graphics.FromImage(img);
Font font = new Font("Alial Black", 40);
Color color = Color.FromArgb(50, 241, 235, 105);
StringFormat stringFormat = new StringFormat
{
Alignment = StringAlignment.Near,
LineAlignment = StringAlignment.Near
};
gr.SmoothingMode = SmoothingMode.AntiAlias;
gr.DrawString(watermarkText, font, new SolidBrush(color),
new Point(20, img.Height - 60), stringFormat);
img.Save(MapPath(GlobalVariables.UploadPath + "/w_" + filePath));
}
Sometimes the font goes off the bottom. I want it to be text along the bottom of the image.
How do I ensure it doesn't go off the bottom?
Also, I want to enhance it slightly. I want to make a white, but transparent bar across the full length of the bottom of the image, and then write black text over it. Is this possible with drawing? So, a bar across the bottom of the image, maybe 60 pixels high, and in the middle of the 60px, I want text written (left aligned).
I am also finding the text moves arounf, depending on the file size
Here's an image that works:
http://www.listerhome.com/fulldisplay.aspx?imageid=100055
Bur sometimes, when I uploaded higher resolution images, I get this:
http://www.listerhome.com/fulldisplay.aspx?imageid=100060
You can use MeasureString function to calculate string size.
SizeF stringSize = gr.MeasureString(watermarkText, font, img.Width - 40);
gr.DrawString(watermarkText, font, new SolidBrush(color),
new RectangleF(20, img.Height - stringSize.Height, img.Width - 40, stringSize.Height),
stringFormat);

Categories

Resources