Identical fonts don't look identical on high DPI monitors - winforms

I've got a WinForms application that works great on older systems, but I'm having trouble making it look good on 4k monitors. There are multiple issues, and a lot written on the subject, but this question is focused on one specific problem. I can set different controls to use the same font, but on high DPI systems, the controls will look a lot different. How can I fix this?
Obviously I can change the font size, move controls around, etc. But Windows is adding a mysterious factor into my font sizes. Without knowing what Windows is doing, it's hard for me to undo it!
On an older system my test window looks perfect:
On a high DPI system, some controls have a different font size than others:
I've tried several things, including manually setting the font on some controls rather than inheriting from the form. As you can see, changing the font did not fix the problem:
After searching the Internet I've tried several things to fix this including:
Changing the application between PROCESS_DPI_UNAWARE, PROCESS_SYSTEM_DPI_AWARE, and PROCESS_PER_MONITOR_DPI_AWARE
Explicitly changing the font rather than using the form's font.
Building on an old system vs building on a high DPI system.
Building on a monitor set to 96 DPI / 100% vs building on a monitor set to 192 DPI / 200% on the same computer.
Building the form in visual studio's designer vs building it in pure C# code.
.Net 4.0 vs. .Net 4.6.1
Visual Studio 2010 vs Visual Studio 2015
I only found one thing that fixed my problem. Unfortunately I had to do it on the target machine, not on the machine where I'm building this. So it's not a practical solution. See the second item under "steps to repeat" for more details.
Steps to repeat:
This happens with a lot of controls on a lot of forms. See the code sample below for a small, simple demo. That's how I got the screenshots, above.
I can make this problem appear or disappear with one system setting. If you change the main monitor to 96 DPI / 100% scaling, then reboot, you'll get the good result where all fonts are as requested. If you change the main monitor to a different DPI setting, then reboot, you'll see the bad results.
private void newFormButton_Click(object sender, EventArgs e)
{
Font copyOfFont = new Font(Font, FontStyle.Strikeout);
Form form = new Form();
form.Font = Font;
string sample = "Abc 123 :)";
int padding = 6;
Label label = new Label();
label.Text = sample;
label.Top = padding;
label.Left = padding;
label.Font = copyOfFont;
label.Parent = form;
Button button = new Button();
button.Text = sample;
button.Top = label.Bottom + padding;
button.Left = padding;
button.Width = label.Width + padding * 2;
button.Height = label.Height + padding * 2;
button.Parent = form;
TextBox textBox = new TextBox();
textBox.Text = sample;
textBox.Size = button.Size;
textBox.Top = button.Bottom + padding;
textBox.Left = padding;
textBox.Parent = form;
ListBox listBox = new ListBox();
listBox.Items.Add(sample);
listBox.Items.Add(sample);
listBox.Width = button.Width;
listBox.Height = button.Height * 2;
listBox.Top = textBox.Bottom + padding;
listBox.Left = padding;
listBox.Font = copyOfFont;
listBox.Parent = form;
form.Show();
}

This is crazy but it works.
Everything I've seen on the internet about DPI Virtualization says that Windows will automatically set a process to PROCESS_DPI_UNAWARE by default. So unless you explicitly pick one of the other two settings, your application should look decent on a high resolution monitor. It might be a little fuzzy, but it shouldn't look as bad as the examples I've shown above.
Apparently that's not true. The default depends on the computer, and it depends on the day. My solution: Explicitly set the application to use PROCESS_DPI_UNAWARE. I've included a code sample below.
Note that you should be able to take care of this using the manifest. Some sources say that's the preferred way, rather than using C# code. We've had mixed results with that. The C# code option seems more reliable.
[DllImport("shcore.dll")]
static extern int SetProcessDpiAwareness(_Process_DPI_Awareness value);
enum _Process_DPI_Awareness
{
Process_DPI_Unaware = 0,
Process_System_DPI_Aware = 1,
Process_Per_Monitor_DPI_Aware = 2
}
public MainForm()
{
//int result = SetProcessDpiAwareness(_Process_DPI_Awareness.Process_System_DPI_Aware);
//int result = SetProcessDpiAwareness(_Process_DPI_Awareness.Process_Per_Monitor_DPI_Aware);
int result = SetProcessDpiAwareness(_Process_DPI_Awareness.Process_DPI_Unaware);
System.Diagnostics.Debug.Assert(result == 0);
This works on a number of different developer machines. We're about to start sending the fix out to beta testers.
Summary
The O/S provides a compatibility mode for old programs running on high DPI systems.
WinForms and the O/S provide tools for manually changing the sizes of your controls depending on the DPI of the system
or the current monitor.
Both #1 and #2 are both seriously buggy!
The details var a lot from one computer to the next.
Fixing #2 would be the more
powerful option, but as far as I can tell it would be impossible to
fix that.
Instead I fixed #1. That works reasonably well.

Related

What would cause a data grid error on an image column only when deployed on a Windows 10 machine?

I have a C# windows forms app with a data grid. One column of the grid is an image column which I put a bitmap in. It works fine on the Windows 10 machine I developed it on and when I deploy it on Windows 7 machines, but on all other Windows 10 machines I get an error. The error is "System.FormatException: Formatted value of the cell has a wrong type." I haven't been able to recreate the error on my development machine, so I'm not sure how to figure out what's wrong. I thought it might be occurring because some prerequisite was not included when I deploy it (it's a Click Once application), but I don't see anything missing. I thought maybe it couldn't find the links to the image, so I tried drawing them instead and still got the same error. I'm guessing it has to do with my bitmap column, but I don't know why it would work on my machine and Windows 7 machines and not other Windows 10 machines. Any ideas on what's happening and/or how to fix this?
Here is the designer for my grid:
Here is the DefaultCellStyle settings. I thought the NullValue might be the cause. I originally had System.Drawing.Bitmap in it. I tried using null, but still get an error.
Here's the code for how I populate that column.
Bitmap bmpCombineOnly = new Bitmap(1, 1);
Bitmap bmpYellow = new Bitmap(26, 26);
Graphics grYellow = Graphics.FromImage(bmpYellow);
grYellow.DrawEllipse(new Pen(Color.Yellow, 2), 0, 0, 25, 25);
grYellow.FillEllipse(new SolidBrush(Color.Yellow), 0, 0, 25, 25);
bmpCombineOnly = bmpYellow;
foreach (DataGridViewRow row in ItemsInLocationDataGridView.Rows)
{
row.Cells["Icon"].Value = bmpCombineOnly;
}

Having issue with slow typing in Windows Forms App

I am writing a Windows Forms App with a ComboBox. It is a DropDownList with SuggestAppend. When the user types, it should position to the item meeting the keyed letters. If they type reasonably fast, this works as expected. If there is even a short delay in keystrokes, it starts over thinking it is a different value. Unfortunately, I have a number of one fingered typists (not even two fingers) and they can't type fast enough.
Here is the code as the designer created it...
this.cbxItemDescription.AutoCompleteMode = System.Windows.Forms.AutoCompleteMode.SuggestAppend;
this.cbxItemDescription.AutoCompleteSource = sysem.Windows.Forms.AutoCompleteSource.ListItems;
this.cbxItemDescription.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
this.cbxItemDescription.FormattingEnabled = true;
this.cbxItemDescription.Location = new System.Drawing.Point(75, 2);
this.cbxItemDescription.Name = "cbxItemDescription";
this.cbxItemDescription.Size = new System.Drawing.Size(300, 21);
this.cbxItemDescription.TabIndex = 2;
this.cbxItemDescription.Visible = false;
this.cbxItemDescription.SelectedIndexChanged += new System.EventHandler(this.cbxItemDescription_SelectedIndexChanged);`
It is loaded by the line...
cbxItemDescription.Items.Add(value to be added);
Can anyone tell me how to extend the interval so it can accommodate these slow typists?
I have found answers for WPF ComboBoxes but unless I am not understanding the solution, they won't work for Windows Forms.

Need to figure out how to use DeepZoomTools.dll to create DZI

I am not familiar with .NET coding.
However, I must create DZI sliced image assets on a shared server and am told that I can instantiate and use DeepZoomTools.dll.
Can someone show me a very simple DZI creation script that demonstrates the proper .NET coding technique? I can embellish as needed, I'm sure, but don't know where to start.
Assuming I have a jpg, how does a script simply slice it up and save it?
I can imagine it's only a few lines of code. The server is running IIS 7.5.
If anyone has a simple example, I'd be most appreciative.
Thanks
I don't know myself, but you might ask in the OpenSeadragon community:
https://github.com/openseadragon/openseadragon/issues
Someone there might know.
Does it have to be DeepZoomTools.dll? There are a number of other options for creating DZI files. Here are a few:
http://openseadragon.github.io/examples/creating-zooming-images/
Example of building a Seadragon Image from multiple images.
In this, the "clsCanvas" objects and collection can pretty much be ignored, it was an object internal to my code that was generating the images with GDI+, then putting them on disk. The code below just shows how to get a bunch of images from file and assemble them into a zoomable collection. Hope this helps someone :-).
CollectionCreator cc = new CollectionCreator();
// set default values that make sense for conversion options
cc.ServerFormat = ServerFormats.Default;
cc.TileFormat = ImageFormat.Jpg;
cc.TileSize = 256;
cc.ImageQuality = 0.92;
cc.TileOverlap = 0;
// the max level should always correspond to the log base 2 of the tilesize, unless otherwise specified
cc.MaxLevel = (int)Math.Log(cc.TileSize, 2);
List<Microsoft.DeepZoomTools.Image> aoImages = new List<Microsoft.DeepZoomTools.Image>();
double fLeftShift = 0;
foreach (clsCanvas oCanvas in aoCanvases)
{
//viewport width as a function of this canvas, so the width of this canvas is 1
double fThisImgWidth = oCanvas.MyImageWidth - 1; //the -1 creates a 1px overlap, hides the seam between images.
double fTotalViewportWidth = fTotalImageWidth / fThisImgWidth;
double fMyLeftEdgeInViewportUnits = -fLeftShift / fThisImgWidth; ; //please don't ask me why this is a negative numeber
double fMyTopInViewportUnits = -fTotalViewportWidth * 0.3;
fLeftShift += fThisImgWidth;
Microsoft.DeepZoomTools.Image oImg = new Microsoft.DeepZoomTools.Image(oCanvas.MyFileName.Replace("_Out_Tile",""));
oImg.ViewportWidth = fTotalViewportWidth;
oImg.ViewportOrigin = new System.Windows.Point(fMyLeftEdgeInViewportUnits, fMyTopInViewportUnits);
aoImages.Add(oImg);
}
// create a list of all the images to include in the collection
cc.Create(aoImages, sMasterOutFile);

Setting System.Windows.Media.FontFamily("Calibri") doesn't work on other PCs

For a WPF application where I need to display numbers in scientific notation (eg., 10² 10ⁿ), I've used code like below. I'm assigning these "Run" variables to a TextBlock, I have to do this from code-behind file in a Converter and hence this C# code and I cannot use XAML.
The issue that I'm noticing is that on my development machine it works fine, displays as expected like 10², whereas in others' machines, I see this as 102, instead of superscript.
Can anyone please let me know how to troubleshoot this or what I'm missing?
Btw, the reason I'm setting the FontFamily specifically is apparently I found out that not all font families support superscripts.
Run logBase = new Run();
logBase.FontFamily = new System.Windows.Media.FontFamily("Calibri");
logBase.Text = "10";
Run logExp = new Run();
logExp.FontFamily = new System.Windows.Media.FontFamily("Calibri");
logExp.Text = "2";
logExp.Typography.Variants = FontVariants.Superscript;
What are the specs of the other machines? What's different about them? Are they on XP? Calibri was first distributed with Vista, so XP machines won't have it by default. Try a different font and see if you have the same issue.
http://en.wikipedia.org/wiki/Calibri

How do I convert a WPF size to physical pixels?

What's the best way to convert a WPF (resolution-independent) width and height to physical screen pixels?
I'm showing WPF content in a WinForms Form (via ElementHost) and trying to work out some sizing logic. I've got it working fine when the OS is running at the default 96 dpi. But it won't work when the OS is set to 120 dpi or some other resolution, because then a WPF element that reports its Width as 96 will actually be 120 pixels wide as far as WinForms is concerned.
I couldn't find any "pixels per inch" settings on System.Windows.SystemParameters. I'm sure I could use the WinForms equivalent (System.Windows.Forms.SystemInformation), but is there a better way to do this (read: a way using WPF APIs, rather than using WinForms APIs and manually doing the math)? What's the "best way" to convert WPF "pixels" to real screen pixels?
EDIT: I'm also looking to do this before the WPF control is shown on the screen. It looks like Visual.PointToScreen could be made to give me the right answer, but I can't use it, because the control isn't parented yet and I get InvalidOperationException "This Visual is not connected to a PresentationSource".
Transforming a known size to device pixels
If your visual element is already attached to a PresentationSource (for example, it is part of a window that is visible on screen), the transform is found this way:
var source = PresentationSource.FromVisual(element);
Matrix transformToDevice = source.CompositionTarget.TransformToDevice;
If not, use HwndSource to create a temporary hWnd:
Matrix transformToDevice;
using(var source = new HwndSource(new HwndSourceParameters()))
transformToDevice = source.CompositionTarget.TransformToDevice;
Note that this is less efficient than constructing using a hWnd of IntPtr.Zero but I consider it more reliable because the hWnd created by HwndSource will be attached to the same display device as an actual newly-created Window would. That way, if different display devices have different DPIs you are sure to get the right DPI value.
Once you have the transform, you can convert any size from a WPF size to a pixel size:
var pixelSize = (Size)transformToDevice.Transform((Vector)wpfSize);
Converting the pixel size to integers
If you want to convert the pixel size to integers, you can simply do:
int pixelWidth = (int)pixelSize.Width;
int pixelHeight = (int)pixelSize.Height;
but a more robust solution would be the one used by ElementHost:
int pixelWidth = (int)Math.Max(int.MinValue, Math.Min(int.MaxValue, pixelSize.Width));
int pixelHeight = (int)Math.Max(int.MinValue, Math.Min(int.MaxValue, pixelSize.Height));
Getting the desired size of a UIElement
To get the desired size of a UIElement you need to make sure it is measured. In some circumstances it will already be measured, either because:
You measured it already
You measured one of its ancestors, or
It is part of a PresentationSource (eg it is in a visible Window) and you are executing below DispatcherPriority.Render so you know measurement has already happened automatically.
If your visual element has not been measured yet, you should call Measure on the control or one of its ancestors as appropriate, passing in the available size (or new Size(double.PositivieInfinity, double.PositiveInfinity) if you want to size to content:
element.Measure(availableSize);
Once the measuring is done, all that is necessary is to use the matrix to transform the DesiredSize:
var pixelSize = (Size)transformToDevice.Transform((Vector)element.DesiredSize);
Putting it all together
Here is a simple method that shows how to get the pixel size of an element:
public Size GetElementPixelSize(UIElement element)
{
Matrix transformToDevice;
var source = PresentationSource.FromVisual(element);
if(source!=null)
transformToDevice = source.CompositionTarget.TransformToDevice;
else
using(var source = new HwndSource(new HwndSourceParameters()))
transformToDevice = source.CompositionTarget.TransformToDevice;
if(element.DesiredSize == new Size())
element.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
return (Size)transformToDevice.Transform((Vector)element.DesiredSize);
}
Note that in this code I call Measure only if no DesiredSize is present. This provides a convenient method to do everything but has several deficiencies:
It may be that the element's parent would have passed in a smaller availableSize
It is inefficient if the actual DesiredSize is zero (it is remeasured repeatedly)
It may mask bugs in a way that causes the application to fail due to unexpected timing (eg. the code being called at or above DispatchPriority.Render)
Because of these reasons, I would be inclined to omit the Measure call in GetElementPixelSize and just let the client do it.
Simple proportion between Screen.WorkingArea and SystemParameters.WorkArea:
private double PointsToPixels (double wpfPoints, LengthDirection direction)
{
if (direction == LengthDirection.Horizontal)
{
return wpfPoints * Screen.PrimaryScreen.WorkingArea.Width / SystemParameters.WorkArea.Width;
}
else
{
return wpfPoints * Screen.PrimaryScreen.WorkingArea.Height / SystemParameters.WorkArea.Height;
}
}
private double PixelsToPoints(int pixels, LengthDirection direction)
{
if (direction == LengthDirection.Horizontal)
{
return pixels * SystemParameters.WorkArea.Width / Screen.PrimaryScreen.WorkingArea.Width;
}
else
{
return pixels * SystemParameters.WorkArea.Height / Screen.PrimaryScreen.WorkingArea.Height;
}
}
public enum LengthDirection
{
Vertical, // |
Horizontal // ——
}
This works fine with multiple monitors as well.
I found a way to do it, but I don't like it much:
using (var graphics = Graphics.FromHwnd(IntPtr.Zero))
{
var pixelWidth = (int) (element.DesiredSize.Width * graphics.DpiX / 96.0);
var pixelHeight = (int) (element.DesiredSize.Height * graphics.DpiY / 96.0);
// ...
}
I don't like it because (a) it requires a reference to System.Drawing, rather than using WPF APIs; and (b) I have to do the math myself, which means I'm duplicating WPF's implementation details. In .NET 3.5, I have to truncate the result of the calculation to match what ElementHost does with AutoSize=true, but I don't know whether this will still be accurate in future versions of .NET.
This does seem to work, so I'm posting it in case it helps others. But if anyone has a better answer, please, post away.
Just did a quick lookup in the ObjectBrowser and found something quite interesting, you might want to check it out.
System.Windows.Form.AutoScaleMode, it has a property called DPI. Here's the docs, it might be what you are looking for :
public const
System.Windows.Forms.AutoScaleMode Dpi
= 2
Member of System.Windows.Forms.AutoScaleMode
Summary: Controls scale relative to
the display resolution. Common
resolutions are 96 and 120 DPI.
Apply that to your form, it should do the trick.
{enjoy}

Resources