How to get DPI scale for all screens? - c#

I need to get the DPI scale, as set from Control Panel > Display, for each of the screens connected to the computer, even those that do not have a WPF window open. I have seen a number of ways to get DPI (for example, http://dzimchuk.net/post/Best-way-to-get-DPI-value-in-WPF) but these seem to be dependent on either Graphics.FromHwnd(IntPtr.Zero) or PresentationSource.FromVisual(visual).CompositionTarget.TransformToDevice.
Is there a way to get the DPI settings for each individual screen?
Background - I am creating a layout configuration editor so that the user can set up their configuration prior to launch. For this, I draw each of the screens relative to each other. For one configuration we are using a 4K display that has a larger than default DPI scale set. It is drawing much smaller than it physically appears in relation to the other screens because it reports as the same resolution as the other screens.

I found a way to get the dpi’s with the WinAPI.
As first needs references to System.Drawing and System.Windows.Forms. It is possible to get the monitor handle with the WinAPI from a point on the display area - the Screen class can give us this points. Then the GetDpiForMonitor function returns the dpi of the specified monitor.
public static class ScreenExtensions
{
public static void GetDpi(this System.Windows.Forms.Screen screen, DpiType dpiType, out uint dpiX, out uint dpiY)
{
var pnt = new System.Drawing.Point(screen.Bounds.Left + 1, screen.Bounds.Top + 1);
var mon = MonitorFromPoint(pnt, 2/*MONITOR_DEFAULTTONEAREST*/);
GetDpiForMonitor(mon, dpiType, out dpiX, out dpiY);
}
//https://msdn.microsoft.com/en-us/library/windows/desktop/dd145062(v=vs.85).aspx
[DllImport("User32.dll")]
private static extern IntPtr MonitorFromPoint([In]System.Drawing.Point pt, [In]uint dwFlags);
//https://msdn.microsoft.com/en-us/library/windows/desktop/dn280510(v=vs.85).aspx
[DllImport("Shcore.dll")]
private static extern IntPtr GetDpiForMonitor([In]IntPtr hmonitor, [In]DpiType dpiType, [Out]out uint dpiX, [Out]out uint dpiY);
}
//https://msdn.microsoft.com/en-us/library/windows/desktop/dn280511(v=vs.85).aspx
public enum DpiType
{
Effective = 0,
Angular = 1,
Raw = 2,
}
There are three types of scaling, you can find a description in the MSDN.
I tested it quickly with a new WPF application:
private void Window_Loaded(object sender, System.Windows.RoutedEventArgs e)
{
var sb = new StringBuilder();
sb.Append("Angular\n");
sb.Append(string.Join("\n", Display(DpiType.Angular)));
sb.Append("\nEffective\n");
sb.Append(string.Join("\n", Display(DpiType.Effective)));
sb.Append("\nRaw\n");
sb.Append(string.Join("\n", Display(DpiType.Raw)));
this.Content = new TextBox() { Text = sb.ToString() };
}
private IEnumerable<string> Display(DpiType type)
{
foreach (var screen in System.Windows.Forms.Screen.AllScreens)
{
uint x, y;
screen.GetDpi(type, out x, out y);
yield return screen.DeviceName + " - dpiX=" + x + ", dpiY=" + y;
}
}
I hope it helps!

Related

this.Region Path data leads to a tiny window

So I created a simple form to test out using an SVG image to draw a custom shaped window. Inspiration found here
It seems to work fine, but no matter what I do my window size is too small to put any controls on.
Reasons for doing this: It's cool? Windows needs better themeing support. I was bored!
I am using Svg from nuget.com from within Visual Studio
Code:
using Svg;
public const int WM_NCLBUTTONDOWN = 0xA1; public const int HT_CAPTION = 0x2;
internal class NativeMethods
{
// Allows forms with Toolbox property set to false to be moved
[System.Runtime.InteropServices.DllImportAttribute("user32.dll", CharSet = CharSet.Unicode)]
internal static extern IntPtr SendMessage(IntPtr hWnd, int Msg, IntPtr wParam, IntPtr lParam);
[System.Runtime.InteropServices.DllImportAttribute("user32.dll", CharSet = CharSet.Unicode)]
internal static extern bool ReleaseCapture();
}
public Form1()
{
InitializeComponent();
}
protected override void OnPaint(PaintEventArgs e)
{
System.Drawing.Drawing2D.GraphicsPath frmshp = new System.Drawing.Drawing2D.GraphicsPath();
//frmshp.AddEllipse(0, 0, this.Width, this.Height);
//SvgDocument.Open(#"TestWindowsshape.svg");
SvgDocument newshp = SvgDocument.Open(#"TestWindowsshape.svg");
frmshp = (newshp.Path);
this.Region = new Region(frmshp);
}
private void Form1_MouseDown(object sender, System.Windows.Forms.MouseEventArgs e)
{
// Make settings window movable without a titlebar
if (e.Button == MouseButtons.Left)
{
NativeMethods.ReleaseCapture();
NativeMethods.SendMessage(Handle, WM_NCLBUTTONDOWN (IntPtr)HT_CAPTION, (IntPtr)0);
}
}
I have tried to increase the svg size, played with the code some, but nothing I do will make the drawn window bigger. I know I can do this with a BMP, and the TransparancyKey option, but I would like to not do it that way, since the BMP & transparency method has the drawback of not being able to use one color in the bitmap itself. Any advice would be appreciated
Edit:
Matrix m = new Matrix();
m.Scale(100, 100, MatrixOrder.Append);
m.Translate(100, 100, MatrixOrder.Append);
newshp.Path.Transform(m);
Has been tried, with no effect. I would assume that this should have worked does that mean the problem is within my SVG?
The problem seems to be in the SVG file, i adjusted the size of a Rectangle in Notepad++ and got a bigger windows, however more complex shapes will be a hassle. It seems Inkscape cannot create SVGs of a reliable size... I have the "Document properties" set to my screen resolution, but the vectors all turn out too small. Perhaps Illustrator can do this properly.

Cursor created via CreateIconIndirect isn't scaled with display DPI

I am creating a non-DPI-aware Winforms application. This application works fine on displays with high DPI scaling; Windows simply scales up the application as it should (it looks a bit fuzzy, but that's fine).
The problem is that the mouse cursor, which is created from a Bitmap object using CreateIconIndirect, is not scaled along with the application window, making it look really tiny.
Here's the code I use to create the mouse cursor from a Bitmap object:
[DllImport("user32.dll")]
private static extern bool GetIconInfo(IntPtr hIcon, out IconInfo pIconInfo);
[DllImport("user32.dll")]
private static extern IntPtr CreateIconIndirect(ref IconInfo icon);
/// <summary>
/// Creates a new Cursor object from the specified bitmap and hotspot.
/// </summary>
private static Cursor CreateCursor(Bitmap bmp, Point hotSpot)
{
IntPtr handle = bmp.GetHicon();
IconInfo tmp = new IconInfo();
GetIconInfo(handle, out tmp);
tmp.xHotspot = hotSpot.X;
tmp.yHotspot = hotSpot.Y;
tmp.fIcon = false;
handle = CreateIconIndirect(ref tmp);
return new Cursor(handle);
}
private struct IconInfo
{
public bool fIcon;
public int xHotspot;
public int yHotspot;
public IntPtr hbmMask;
public IntPtr hbmColor;
}
So my question is: How can I get a cursor created using CreateIconIndirect to scale properly with the display's DPI scaling, without make my application DPI-aware?
I initially thought that I could simply get the display's DPI scaling and upscale my Bitmap myself, but none of the solutions for retrieving DPI in the following two posts work for me, presumably because my app isn't DPI-aware and would need to retrieve the DPI for the specific display on which the app is running.
How to get Windows Display settings?
How To Get DPI in C# .NET
This is on Windows 10 x64 using .Net 4.6.
Thanks!

Menu out of place when application is in full screen by Windows API

I have developed an application in C# and I want to show it in full screen mode. It should also cover up the taskbar. To accomplish this I have used the Windows API. You can find the class below:
public sealed class WinAPI
{
[DllImport("user32.dll", EntryPoint = "GetSystemMetrics")]
public static extern int GetSystemMetrics(int which);
[DllImport("user32.dll")]
public static extern void
SetWindowPos(IntPtr hwnd, IntPtr hwndInsertAfter,
int X, int Y, int width, int height, uint flags);
private const int SM_CXSCREEN = 0;
private const int SM_CYSCREEN = 1;
private static IntPtr HWND_TOP = IntPtr.Zero;
private const int SWP_SHOWWINDOW = 64; // 0×0040
public static int ScreenX
{
get { return GetSystemMetrics(SM_CXSCREEN); }
}
public static int ScreenY
{
get { return GetSystemMetrics(SM_CYSCREEN); }
}
public static void SetWinFullScreen(IntPtr hwnd)
{
SetWindowPos(hwnd, HWND_TOP, 0, 0, ScreenX, ScreenY, SWP_SHOWWINDOW);
}
}
I am using this class in conjunction with the following form settings to go in full screen mode:
private void terminalModeToolStripMenuItem_Click(object sender, EventArgs e)
{
// Remove the border.
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.None;
this.WindowState = FormWindowState.Maximized;
this.Bounds = Screen.PrimaryScreen.Bounds;
// Full screen windows API hack.
WinAPI.SetWinFullScreen(this.Handle);
}
Now comes the funny part. If I click a button in my menu bar it will show up with a gap between the button and the menu as you can see in the image below:
Does anyone knows how to fix this issue? I would like it to show up like this:
And why does this happen anyway?
As Muraad pointed you to in his comment, try moving the following block of code into your Form load event:
// Remove the border.
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.None;
this.WindowState = FormWindowState.Maximized;
this.Bounds = Screen.PrimaryScreen.Bounds;
And see if the issue still persists.
This problem is caused by having the task bar set to the top of the screen.
It can be resolved by either moving the task bar to the bottom of the screen, or by enabling the Auto-hide task bar check box in the Properties window of the task bar.
EDIT: As stated by #slashp 's comments. The root cause of this issue comes from some inner mechanics in drawing the menu. The menu has a safety to be always drawn within the working area. Which seems to be your screen - task bar. Because the application is placed over the task bar, the calculation is placing the menu below the task bar. (you can't see it, but it's still there)

Setting calendar size when overriding DateTimePicker to add week numbers

I'm trying to create a DateTimePicker with week numbers displayed, as shown here (Code project example).
It works fairly well, except for one minor detail; The calender popping up when trying to select a date is not the right size. As you can see, the calendar area is sort of "cramped", especially along the right edge.
I can click the bottom right corner here, and drag it out a little - just enough to expand it so that it looks right:
I can't seem to find any way to force the calendar to be the correct/full size from the beginning, or to resize it.
Finally found a solution that seems to work - at least for now.
It seems there are two windows in the calendar part of the DateTimePicker. Apparently my code would automatically find the correct size for the inner one (more or less at least?), but not the outer one.
A bit of research has led to the code below. The following links provide some useful and relevant info:
GetWindowLong function (Used for getting info about the window to edit)
GetParent function (Finding the outer window, so we could apply settings to that too)
The trick was to add a little to the height and width of the (inner) window, then apply the same height and width to the outer window (which I access using the GetParrent() function). I found the "correct" size by trial and error: When the size matched what was needed for the contents of the calendar, it could not be resized any longer.
Yes, this feels a little like a hack, and no, I haven't been able to verify that it works perfectly on other computers than my own yet. I'm a little worried about having to give specific values for height and width, but I'm hoping this won't be affected by screen resolutions or whatever else.
Hope someone else in a similar situation will find the code useful.
(The following can directly replace a regular DateTimePicker to show week numbers in the calendar)
using System;
using System.ComponentModel;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Windows.Forms;
public class DatePickerWithWeekNumbers : DateTimePicker
{
[DllImport("User32.dll")]
private static extern int GetWindowLong(IntPtr handleToWindow,
int offsetToValueToGet);
[DllImport("User32.dll")]
private static extern int SetWindowLong(IntPtr h,
int index,
int value);
private const int McmFirst = 0x1000;
private const int McmGetminreqrect = (McmFirst + 9);
private const int McsWeeknumbers = 0x4;
private const int DtmFirst = 0x1000;
private const int DtmGetmonthcal = (DtmFirst + 8);
[DllImport("User32.dll")]
private static extern IntPtr SendMessage(IntPtr h,
int msg,
int param,
int data);
[DllImport("User32.dll")]
private static extern IntPtr GetParent(IntPtr h);
[DllImport("User32.dll")]
private static extern int SendMessage(IntPtr h,
int msg,
int param,
ref Rectangle data);
[DllImport("User32.dll")]
private static extern int MoveWindow(IntPtr h,
int x,
int y,
int width,
int height,
bool repaint);
[Browsable(true), DesignerSerializationVisibility(
DesignerSerializationVisibility.Visible)]
public bool DisplayWeekNumbers { get; set; }
protected override void OnDropDown(EventArgs e)
{
// Hex value to specify that we want the style-attributes
// for the window:
const int offsetToGetWindowsStyles = (-16);
IntPtr pointerToCalenderWindow = SendMessage(Handle,
DtmGetmonthcal,
0,
0);
int styleForWindow = GetWindowLong(pointerToCalenderWindow,
offsetToGetWindowsStyles);
// Check properties for the control - matches available
// property in the graphical properties for the DateTimePicker:
if (DisplayWeekNumbers)
{
styleForWindow = styleForWindow | McsWeeknumbers;
}
else
{
styleForWindow = styleForWindow & ~McsWeeknumbers;
}
// Get the size needed to display the calendar (inner window)
var rect = new Rectangle();
SendMessage(pointerToCalenderWindow, McmGetminreqrect, 0, ref rect);
// Add to size as needed (I don't know why
// this was not correct initially!)
rect.Width = rect.Width + 28;
rect.Height = rect.Height + 6;
// Set window styles..
SetWindowLong(pointerToCalenderWindow,
offsetToGetWindowsStyles,
styleForWindow);
// Dont move the window - just resize it as needed:
MoveWindow(pointerToCalenderWindow,
0,
0,
rect.Right,
rect.Bottom,
true);
// Now access the parrent window..
var parentWindow = GetParent(pointerToCalenderWindow);
// ...and resize that the same way:
MoveWindow(parentWindow, 0, 0, rect.Right, rect.Bottom, true);
base.OnDropDown(e);
}
}
For me, setting MCS_WEEKNUMBERS via the DateTimePicker's DTM_SETMCSTYLE message automatically resulted in the correct size of the MonthCal control:
SendMessage(Handle, DTM_FIRST + 11, 0, SendMessage(Handle, DTM_FIRST + 12, 0, 0) | MCS_WEEKNUMBERS);
Where DTM_FIRST = 0x1000 and MCS_WEEKNUMBERS = 0x4 as in Kjartan's solution. DTM_FIRST + 11 is DTM_SETMCSTYLE and DTM_FIRST + 12 is DTM_GETMCSTYLE in Microsoft's documentation.
Unlike Kjartan's solution, this call must be used before the first dropdown, but right at form initialization didn't work for me in some cases, so I delayed it to when the form was already created and visible in these cases. One call is enough, the DateTimePicker will save the style for future dropdowns.
Ok, Try to comment line in Program.cs
Application.EnableVisualStyles();
and then try to execute.

Unable to Calculate Position within Owner-Draw Text

I'm trying to use Visual Studio 2012 to create a Windows Forms application that can place the caret at the current position within a owner-drawn string. However, I've been unable to find a way to accurately calculate that position.
I've done this successfully before in C++. I've tried numerous methods in C# but have not yet been able to position the caret accurately. Originally, I tried using .NET classes to determine the correct position, but then I tried accessing the Windows API directly. In some cases, I came close, but after some time I still cannot place the caret accurately.
I've created a small test program and posted key parts below. I've also posted the entire project here.
The exact font used is not important to me; however, my application assumes a mono-spaced font. Any help is appreciated.
Form1.cs
This is my main form.
public partial class Form1 : Form
{
private string TestString;
private int AveCharWidth;
private int Position;
public Form1()
{
InitializeComponent();
TestString = "123456789012345678901234567890123456789012345678901234567890";
AveCharWidth = GetFontWidth();
Position = 0;
}
private void Form1_Load(object sender, EventArgs e)
{
Font = new Font(FontFamily.GenericMonospace, 12, FontStyle.Regular, GraphicsUnit.Pixel);
}
protected override void OnGotFocus(EventArgs e)
{
Windows.CreateCaret(Handle, (IntPtr)0, 2, (int)Font.Height);
Windows.ShowCaret(Handle);
UpdateCaretPosition();
base.OnGotFocus(e);
}
protected void UpdateCaretPosition()
{
Windows.SetCaretPos(Padding.Left + (Position * AveCharWidth), Padding.Top);
}
protected override void OnLostFocus(EventArgs e)
{
Windows.HideCaret(Handle);
Windows.DestroyCaret();
base.OnLostFocus(e);
}
protected override void OnPaint(PaintEventArgs e)
{
e.Graphics.DrawString(TestString, Font, SystemBrushes.WindowText,
new PointF(Padding.Left, Padding.Top));
}
protected override bool IsInputKey(Keys keyData)
{
switch (keyData)
{
case Keys.Right:
case Keys.Left:
return true;
}
return base.IsInputKey(keyData);
}
protected override void OnKeyDown(KeyEventArgs e)
{
switch (e.KeyCode)
{
case Keys.Left:
Position = Math.Max(Position - 1, 0);
UpdateCaretPosition();
break;
case Keys.Right:
Position = Math.Min(Position + 1, TestString.Length);
UpdateCaretPosition();
break;
}
base.OnKeyDown(e);
}
protected int GetFontWidth()
{
int AverageCharWidth = 0;
using (var graphics = this.CreateGraphics())
{
try
{
Windows.TEXTMETRIC tm;
var hdc = graphics.GetHdc();
IntPtr hFont = this.Font.ToHfont();
IntPtr hOldFont = Windows.SelectObject(hdc, hFont);
var a = Windows.GetTextMetrics(hdc, out tm);
var b = Windows.SelectObject(hdc, hOldFont);
var c = Windows.DeleteObject(hFont);
AverageCharWidth = tm.tmAveCharWidth;
}
catch
{
}
finally
{
graphics.ReleaseHdc();
}
}
return AverageCharWidth;
}
}
Windows.cs
Here are my Windows API declarations.
public static class Windows
{
[Serializable, StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
public struct TEXTMETRIC
{
public int tmHeight;
public int tmAscent;
public int tmDescent;
public int tmInternalLeading;
public int tmExternalLeading;
public int tmAveCharWidth;
public int tmMaxCharWidth;
public int tmWeight;
public int tmOverhang;
public int tmDigitizedAspectX;
public int tmDigitizedAspectY;
public short tmFirstChar;
public short tmLastChar;
public short tmDefaultChar;
public short tmBreakChar;
public byte tmItalic;
public byte tmUnderlined;
public byte tmStruckOut;
public byte tmPitchAndFamily;
public byte tmCharSet;
}
[DllImport("user32.dll")]
public static extern bool CreateCaret(IntPtr hWnd, IntPtr hBitmap, int nWidth, int nHeight);
[DllImport("User32.dll")]
public static extern bool SetCaretPos(int x, int y);
[DllImport("User32.dll")]
public static extern bool DestroyCaret();
[DllImport("User32.dll")]
public static extern bool ShowCaret(IntPtr hWnd);
[DllImport("User32.dll")]
public static extern bool HideCaret(IntPtr hWnd);
[DllImport("gdi32.dll", CharSet = CharSet.Auto)]
public static extern bool GetTextMetrics(IntPtr hdc, out TEXTMETRIC lptm);
[DllImport("gdi32.dll")]
public static extern IntPtr SelectObject(IntPtr hdc, IntPtr hgdiobj);
[DllImport("GDI32.dll")]
public static extern bool DeleteObject(IntPtr hObject);
}
Edit
The code I've posted has an issue that makes it even more inaccurate. This is a result of trying many different approaches, some more accurate than this. What I'm looking for is a fix that makes it "fully accurate", as it is in my MFC Hex Editor Control in C++.
I tried out your GetFontWidth(), and the width of a character returned was 7.
I then tried out TextRenderer.MearureText on varying lengths of text and had values ranging from 14 through to 7.14 for text of length 1 to 50 respectively with an average character width of 7.62988874736612.
Here is the code I used:
var text = "";
var sizes = new System.Collections.Generic.List<double>();
for (int i = 1; i <= 50; i++)
{
text += (i % 10).ToString();
var ts = TextRenderer.MeasureText(text, this.Font);
sizes.Add((ts.Width * 1.0) / text.Length);
}
sizes.Add(sizes.Average());
Clipboard.SetText(string.Join("\r\n",sizes));
Not satisfied with the results of my little 'experiment', I decided to see how the text was rendered onto the form. Below is a screen capture of the form (magnified 8x).
On close inspection, I observed that
There was an amount of separation between the characters. This made the length of a block of text (1234567890) is 74 pixels long.
There is some space (3px) in front of the text being drawn even though the left padding is 0.
What does this mean to you?
If you use your code to calculate the width of a font character, you fail to account for the separating space between two characters.
Using the TextRenderer.DrawText can give you varying character widths rendering it quite uselesss.
What are your remaining options?
The best way I can see out of this is to hard-code the placement of your text. That way you know the position of each character and can accurately place the cursor at any desired location.
Needless to say, this is likely going to call for a lot of code.
Your second option is to run tests like I did to find the length of a block of text and then divide by the length of the block to find the average character width.
The problem with this is that your code is not likely to scale properly. For example, changing the size of the font or the user's screen DPI can cause a lot of trouble for the program.
Other things I observed
The space inserted in-front of the text is equivalent to the width of the caret (2px in my case) plus 1px (Total of 3px).
Hard-coding the width of each character to 7.4 works perfectly.
You can use the System.Windows.Forms.TextRenderer to in order to draw the string as well as to calculate its metrics. Various method overloads for both operations exist
TextRenderer.DrawText(e.Graphics, "abc", font, point, Color.Black);
Size measure = TextRenderer.MeasureText(e.Graphics, "1234567890", font);
I have made good experiences with TextRenderer and its accuracy.
UPDATE
I determined the font size like this in one of my applications and it worked perfectly
const TextFormatFlags textFormatFlags =
TextFormatFlags.NoPadding | TextFormatFlags.NoPrefix |
TextFormatFlags.PreserveGraphicsClipping;
fontSize = TextRenderer.MeasureText(this.g, "_", font,
new Size(short.MaxValue, short.MaxValue),
textFormatFlags);
height = fontSize.Height;
width = fontSize.Width;
Make sure to use the same format flags for both drawing and measuring.
(This way of determining the font size of cause works only for monospaced fonts.)

Categories

Resources