Getting actual screen dpi/ppi under windows - c++

I would like to get the actual screen dpi/ppi, not the dpi setting used for font in C++.
I tried with the following codes:
Version 1, reports 72 dpi, which is wrong.
SetProcessDPIAware(); //true
HDC screen = GetDC(NULL);
double hSize = GetDeviceCaps(screen, HORZSIZE);
double vSize = GetDeviceCaps(screen, VERTSIZE);
double hRes = GetDeviceCaps(screen, HORZRES);
double vRes = GetDeviceCaps(screen, VERTRES);
double hPixelsPerInch = hRes / hSize * 25.4;
double vPixelsPerInch = vRes / vSize * 25.4;
ReleaseDC(NULL, screen);
return (hPixelsPerInch + vPixelsPerInch) * 0.5;
Version 2, reports 96 dpi, which is the Windows dpi setting for font, but not the actual screen dpi.
SetProcessDPIAware(); //true
HDC screen = GetDC(NULL);
double hPixelsPerInch = GetDeviceCaps(screen,LOGPIXELSX);
double vPixelsPerInch = GetDeviceCaps(screen,LOGPIXELSY);
ReleaseDC(NULL, screen);
return (hPixelsPerInch + vPixelsPerInch) * 0.5;

I'm honestly confused by the answers here.
Microsoft has a GetDpiForMonitor method:
https://msdn.microsoft.com/en-us/library/windows/desktop/dn280510(v=vs.85).aspx
And monitors DO expose their physical dimensions to tools. You can read your monitors width and height, in centimeters, using the HWiNFO64 tool. So if they're getting it (DDI?), it stands to reason that you can access that information yourself.
Even a different Stack Overflow post mentions using WmiMonitorBasicDisplayParams to get the data.
How to get monitor size
So the top post is flat-out, 100%, wrong.

What you're asking for is, unfortunately, not possible in the general case.
Windows doesn't know the physical screen size. Windows might know that your screen has 1024x768 pixels, but it doesn't know how big the screen actually is. You might pull the cable out of your old 13" screen and connect it to a 19" monitor without changing the resolution. The DPI would be different, but Windows won't notice that you changed monitors.
You can get the true physical dimensions and DPI for a printer (assuming the driver isn't lying), but not for a screen. At least not reliably.
UPDATED
As others have pointed out, there are standards for two-way communication between newer monitors and the OS (EDID), that might make this information available for some devices. But I haven't yet found a monitor that provides this information.
Even if EDID were universally available, it's still not solvable in the general case, as the display could be a video projector, where the DPI would depend on the zoom, the focus, the lens type, and the throw distance. A projector is extremely unlikely to know the throw distance, so there's no way for it to report the actual DPI.

Getting DPI information is found to produce exact value using the below method.
ID2D1Factory* m_pDirect2dFactory;
D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, &m_pDirect2dFactory);
FLOAT dpiX, dpiY;
m_pDirect2dFactory->GetDesktopDpi( &dpiX, &dpiY );

I think what you're after is:
GetDeviceCaps(hdcScreen, LOGPIXELSX);
GetDeviceCaps(hdcScreen, LOGPIXELSY);

Related

Detect Split Screen mode in windows 8

How to detect split screen mode in windows 8. I have a wim32 desktop application(written in MFC) and i need to provide some functionality in case of split screen mode
FYI - In split screen mode both desktop and metro mode come side by side
From your comments, the reason you're getting the screen size is because that's what you're asking for. Passing SM_CXSCREEN and SM_CYSCREEN to GetSystemMetrics() will return, as the name suggests, the width and height of the primary display.
There are a number of solutions, each with their pro's and con's, the simplest of which is probably:
RECT rcDesktop;
BOOL ok = GetWindowRect(GetDesktopWindow(), &rcDesktop);
This will return the size of the desktop window of the primary monitor. If you wanted just the "useable" area (taking into account the taskbar):
RECT rc;
BOOL ok = SystemParametersInfo(SPI_GETWORKAREA, 0, &rc, 0);
In the case of having a Modern-UI app docked to the side of the screen, both of those should return what you want, depending on whether you want to cover the taskbar with your program or not.
Note that those examples will only return information for the primary monitor on multi-monitor systems. You can get information about a specific monitor, such as the monitor that your current window is located on, by doing the following:
MONITORINFO mon_info;
mon_info.cbSize = sizeof(MONITORINFO);
BOOL ok = GetMonitorInfo(MonitorFromWindow(hWnd, MONITOR_DEFAULTTONEAREST), &mon_info);
The MONITORINFO structure contains the size (and position - don't assume it's 0,0) of the requested monitor, including the work area:
Caveat: I'm not at home on my Windows8 system, so I can't check that all of these will return the correct information, but in theory checking the work area should do what you want, unless you specifically want your program to be full-screen.

CreateCompatibleBitmap failing on Windows mobile 6

I'm porting an application from Windows Mobile 2003 to Windows Mobile 6, under Visual Studio 2008. The target device has a VGA resolution screen, and I was surprised to find that the following code fails;
CClientDC ClientDC(this);
CRect Rect;
GetClientRect(&Rect);
int nWidth = Rect.Width(),nHeight = Rect.Height();
CBitmap Temp;
if (!Temp.CreateCompatibleBitmap(&ClientDC,nWidth,nHeight))
{
LogError(elvl_Debug,_T("Error creating bitmap (%s)"),LastSysError());
} else
{
BITMAP bmpinfo;
Temp.GetBitmap(&bmpinfo);
}
The return code from CreateCompatibleBitmap is 8, which translates to 'Not enough memory to process command. nWidth is 350, nHeight is 400, and the display is 16 bits per pixel, so my bitmap is a whopping 280K. The device I'm using has 256mb of program memory, and I've told the linker to reserve 4mb of stack and 64mb of heap. Any ideas what I'm doing wrong, and more importantly a solution? I've been using code similar to the above on Windows CE since CE 2.1 with no problems.
Edit: As per Josh Kelly's post, I moved to device independent bitmaps which works fine on the device. Code is now something like this
CClientDC ClientDC(this);
CRect Rect;
GetClientRect(&Rect);
int nWidth = Rect.Width(),nHeight = Rect.Height();
BITMAPINFOHEADER bmi = { sizeof(bmi) };
bmi.biWidth = nWidth;
bmi.biHeight = nHeight;
bmi.biPlanes = 1;
bmi.biBitCount = 8;
HDC hdc = CreateCompatibleDC(NULL);
BYTE* pbData = 0;
HBITMAP DIB = CreateDIBSection(hdc, (BITMAPINFO*)&bmi, DIB_RGB_COLORS, (void**)&pbData, NULL, 0);
CBitmap *pTempBitmap = CBitmap::FromHandle(DIB);
I haven't done any Windows CE / Windows Mobile programming, but I have dealt with a similar problem (CreateCompatibleBitmap failing with ERROR_NOT_ENOUGH_MEMORY) in desktop Windows. Apparently, from what I've been able to tell from looking around online, Windows may enforce global limitations on the available memory for device dependent bitmaps. (For example, some video drivers may choose to store device dependent bitmaps in video RAM, in which case you're limited by how much RAM is on your video card.) See, for example, this thread. From what I can tell, these limits are determined by the individual video cards or drivers; some computers' storage may be effectively unlimited, others may have strict limits.
One solution is to use device independent bitmaps instead, even though they have a slight performance penalty.

Performing full screen grab in windows

I am working on an idea that involves getting a full capture of the screen including windows and apps, analyzing it, and then drawing items back onto the screen, as an overlay.
I want to learn image processing techniques and I could get lots of data to work with if I can directly access the Windows screen. I could use this to build automation tools the likes of which have never been seen before. More on that later.
I have full screen capture working for the most part.
HWND hwind = GetDesktopWindow();
HDC hdc = GetDC(hwind);
int resx = GetSystemMetrics(SM_CXSCREEN);
int resy = GetSystemMetrics(SM_CYSCREEN);
int BitsPerPixel = GetDeviceCaps(hdc,BITSPIXEL);
HDC hdc2 = CreateCompatibleDC(hdc);
BITMAPINFO info;
info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
info.bmiHeader.biWidth = resx;
info.bmiHeader.biHeight = resy;
info.bmiHeader.biPlanes = 1;
info.bmiHeader.biBitCount = BitsPerPixel;
info.bmiHeader.biCompression = BI_RGB;
void *data;
hbitmap = CreateDIBSection(hdc2,&info,DIB_RGB_COLORS,(void**)&data,0,0);
SelectObject(hdc2,hbitmap);
Once this is done, I can call this repeatedly:
BitBlt(hdc2,0,0,resx,resy,hdc,0,0,SRCCOPY);
The cleanup code (I have no idea if this is correct):
DeleteObject(hbitmap);
ReleaseDC(hwind,hdc);
if (hdc2) {
DeleteDC(hdc2);
}
Every time BitBlt is called it grabs the screen and saves it in memory I can access thru data.
Performance is somewhat satisfactory. BitBlt executes in 50 milliseconds (sometimes as low as 33ms) at 1920x1200x32.
What surprises me is that when I switch display mode to 16 bit, 1920x1200x16, either through my graphics settings beforehand, or by using ChangeDisplaySettings, I get a massively improved screen grab time between 1ms and 2ms, which cannot be explained by the factor of two reduction in bit-depth. Using CreateDIBSection (as above) offers a significant speed up when in 16-bit mode, compared to if I set up with CreateCompatibleBitmap (6-7ms/f).
Does anybody know why dropping to 16bit causes such a speed increase? Is there any hope for me to grab 32bit at such speeds? if not for the color depth, but for not forcing a change of screen buffer modes and the awful flickering.
I solved my original question about the incredible speed up of switching to 16 bit color mode. Turns out it was causing the Aero theme to be disabled, which accounts for most of it. With Aero off in 32bit it is just about as fast.
Link to other topic with a good answer.

Windows GDI: horizontal/vertical DPI

When obtaining the DPI for the screen under Windows (by using ::GetDeviceCaps) will the horizontal value always be the same as the vertical? For example:
HDC dc = ::GetDC(NULL);
const int xDPI = ::GetDeviceCaps(dc, LOGPIXELSX);
const int yDPI - ::GetDeviceCaps(dc, LOGPIXELSY);
assert(xDPI == yDPI);
::ReleaseDC(NULL, dc);
Are these values ever different?
It's possible for it to be different, but that generally only applies to printers. It can be safely assumed that the screen will always have identical horizontal and vertical DPIs.
I have never seen them be different, but on this MSDN page I see a comment that suggests that they might be:
int nHorz = dc.GetDeviceCaps(LOGPIXELSX);
int nVert = dc.GetDeviceCaps(LOGPIXELSY);
// almost always the same in both directions, but sometimes not!
I've never seen a case where they're different, but the fact that there are two separate calls for it strongly suggests that they might be sometimes.
Its easy for them to be different if the monitor is set up to use a screen resolution ratio that is not the same as the physical screen ratio, such as a 4:3 resolution like 1600x1200 on a 16:9 display.

Can't detect when Windows Font Size has changed C++ MFC

I'm trying to determine how I can detect when the user changes the Windows Font Size from Normal to Extra Large Fonts, the font size is selected by executing the following steps on a Windows XP machine:
Right-click on the desktop and select Properties.
Click on the Appearance Tab.
Select the Font Size: Normal/Large Fonts/Extra Large Fonts
My understanding is that the font size change results in a DPI change, so here is what I've tried so far.
My Goal:
I want to detect when the Windows Font Size has changed from Normal to Large or Extra Large Fonts and take some actions based on that font size change. I assume that when the Windows Font Size changes, the DPI will also change (especially when the size is Extra Large Fonts
What I've tried so far:
I receive several messages including: WM_SETTINGCHANGE, WM_NCCALCSIZE, WM_NCPAINT, etc... but none of these messages are unique to the situation when the font size changes, in other words, when I receive the WM_SETTINGSCHANGE message I want to know what changed.
In theory when I define the OnSettingChange and Windows calls it, the lpszSection should tell me what the changing section is, and that works fine, but then I check the given section by calling SystemParametersInfo and I pass in the action SPI_GETNONCLIENTMETRICS, and I step through the debugger and I make sure that I watch the data in the returned NONCLIENTMETRICS for any font changes, but none occur.
Even if that didn't work, I should still be able to check the DPI when the Settings change. I really wouldn't care about the other details, every time I get the WM_SETTINGCHANGE message, I would just check the DPI and perform the actions I'm interested in performing, but I'm not able to get the system DPI either.
I have tried to get the DPI by invoking the method GetSystemMetrics, also for each DC:
Dekstop DC->GetDeviceCaps LOGPIXELSX/LOGPIXELSY
Window DC->GetDeviceCaps LOGPIXELSX/LOGPIXELSY
Current DC->GetDeviceCaps LOGPIXELSX/LOGPIXELSY
Even if I change the DPI in the Graphic Properties Window these values don't return anything different, they always show 96.
Could anybody help me figure this out please? What should I be looking for? Where should I be looking at?
afx_msg void CMainFrame::OnSettingChange(UINT uFlags, LPCTSTR lpszSection)
{
int windowDPI = 0;
int deviceDPI = 0;
int systemDPI = 0;
int desktopDPI = 0;
int dpi_00_X = 0;
int dpi_01_X = 0;
int dpi_02_X = 0;
int dpi_03_X = 0;
CDC* windowDC = CWnd::GetWindowDC(); // try with window DC
HDC desktop = ::GetDC(NULL); // try with desktop DC
CDC* device = CWnd::GetDC(); // try with current DC
HDC hDC = *device; // try with HDC
if( windowDC )
{
windowDPI = windowDC->GetDeviceCaps(LOGPIXELSY);
// always 96 regardless if I change the Font
// Size to Extra Large Fonts or keep it at Normal
dpi_00_X = windowDC->GetDeviceCaps(LOGPIXELSX); // 96
}
if( desktop )
{
desktopDPI = ::GetDeviceCaps(desktop, LOGPIXELSY); // 96
dpi_01_X = ::GetDeviceCaps(desktop, LOGPIXELSX); // 96
}
if( device )
{
deviceDPI = device->GetDeviceCaps(LOGPIXELSY); // 96
dpi_02_X = device->GetDeviceCaps(LOGPIXELSX); // 96
}
systemDPI = ::GetDeviceCaps(hDC, LOGPIXELSY); // 96
dpi_03_X = ::GetDeviceCaps(hDC, LOGPIXELSX); // 96
CWnd::ReleaseDC(device);
CWnd::ReleaseDC(windowDC);
::ReleaseDC(NULL, desktop);
::ReleaseDC(NULL, hDC);
CWnd::OnWinSettingChange(uFlags, lpszSection);
}
The DPI always returns 96, but the settings changes DO take effect when I change the font size to Extra Large Fonts or if I change the DPI to 120 (from the graphics properties).
[EDIT after re-read] I'm almost positive that changing to "Large fonts" does not cause a DPI change, rather it's a theme setting. You should be able to verify by applying the "Large fonts" change and then opening the advanced display properties where the DPI setting lives, it should have remained at 96dpi.
DPI change is supposed to require a reboot. Maybe the setting hasn't propagated to a place where GetDeviceCaps can retrieve it?
Maybe try changing a non-reboot-requiring setting (resolution perhaps) and then see if you can detect the change. If you can, your answer is probably that you can't detect DPI change until after reboot.
When you call GetDeviceCaps() on the Desktop DC, are you perhaps using a DC that might be cached by MFC, and therefore contains out-of-date information? Are you making the GetDeviceCaps() call synchronously from inside your OnSettingsChange handler? I could see how either or both of these things might get you an out of date version of DPI.
Raymond Chen wrote about this and his solution looked like this (Note that I've added :: operators to avoid calling the MFC wrappers of the APIs):
int GetScreenDPI()
{
HDC hdcScreen = ::GetDC(NULL);
int iDPI = -1; // assume failure
if (hdcScreen) {
iDPI = ::GetDeviceCaps(hdcScreen, LOGPIXELSX);
::ReleaseDC(NULL, hdcScreen);
}
return iDPI;
}
I have a hunch WM_THEMECHANGED will take care of you. It doesn't have any hinting about what changed, though. You'll have to use OpenThemeData and cache initial settings, then compare every time you get the message.
You probably don't need to care what changed though, can't you have a general-purpose layout routine that adjusts your form/dialog/whatever by taking everything into account and assumes starting from scratch?
What problem are you trying to solve?
See http://msdn.microsoft.com/en-us/library/ms701681(VS.85).aspx , this is explained there (quote: "If you do not cancel dpi scaling, this call returns the default value of 96 dpi.")
I don't think the display DPI changes when the font size changes. Windows is probably just sending the WM_PAINT and WM_NCPAINT messages to all open windows, and they're redrawing themselves using the current (now large) system font.
Look at these values in the Registry:
Windows XP Theme
HKCU\Software\Microsoft\Windows\CurrentVersion\ThemeManager\SizeName
Possible values: NormalSize, LargeFonts, and ExtraLargeFonts
These values are language-independent.
Windows Classic Theme
HKCU\Control Panel\Appearance\Current
Possible values: Windows Classic, Windows Classic (large), Windows Classic (extra large), Windows Standard, Windows Standard (large), Windows Standard (extra large)
Note that these values are language-dependent.
Windows Vista doesn't support this feature. If we want a bigger font, simply change the DPI Setting. In that case, GetDeviceCaps should work.
Hope this helps.