14

The actual problem I'm trying to solve is, I want to automatically find out the size of the margins around windows. If you can find a better way, please by all means answer that instead of this.

To do this I decided to take a screenshot of a test window and measure the margins. This is simple enough, as I expect no margins will ever be bright pink, but I admit it's a hack. I use GetWindowRect (py) to get the bounding box, and PIL to grab a screenshot and crop to the bounding box. The problem is that while the crop operates correctly, the bounding box is not accurate. The Windows 7 "Snipping Tool" gets the correct dimensions. How may I do the same?

Devin Jeanpierre
  • 92,913
  • 4
  • 55
  • 79

6 Answers6

19

My first thoughts were listed below but if, as you state, you're certain that GetWindowRect is returning incorrect values, see RESOLUTION further down.


"What's wrong with GetSystemMetrics(SM_CXBORDER) and GetSystemMetrics(SM_CYBORDER)?

The method you're using seems a very roundabout way of doing it and, if you can call GetWindowRect(), I'm pretty certain you can call GetSystemMetrics() as well.

One other possibility is to use GetWindowRect to get the entire bounding rectangle for the window and GetClientRect to get the bounding rectangle for the client (non-border) area.

This should give you something like (100,200),(1000,900) and (112,227),(988,888) respectively and you can work out the top border as 227-200, bottom as 900-888, left as 112-100 and right as 900-888 (27,12,12,12).


RESOLUTION:

A bit of investigation turns up this. It's a thread from 2006 stating that you might not get the correct values from GetWindowsRect. The thread that pointed me to this stated:

Apps under Vista that are not linked with WINVER=6 will receive a misleading set of values here, that do not account for the extra padding of "glass" pixels Vista Aero applies to the window. This appears to happen even in Aero Basic (without Glass) to retain sizing consistency. The workaround (if you don't want to set WINVER=6) seems to be to dynamically bind to dwmapi.dll and use GetProcAddress() to obtain the DwmGetWindowAttribute() function, and call it with the DWMWA_EXTENDED_FRAME_BOUNDS argument to request the genuine window frame dimensions.

So basically, use something like (you may have to use ctypes to do this from Python):

RECT r;
HRESULT stat = DwmGetWindowAttribute (
    hwnd,
    DWMWA_EXTENDED_FRAME_BOUNDS,
    &r,
    sizeof(r));

and that should give you the correct bounding rectangle.

Blorgbeard
  • 101,031
  • 48
  • 228
  • 272
paxdiablo
  • 854,327
  • 234
  • 1,573
  • 1,953
  • Well, it returns 1. 1 is not the width of any of the borders on that window. I'm pretty sure the values I want for that window are 8 and 30. – Devin Jeanpierre Jul 07 '10 at 06:04
  • How do you get 8 and 30? The correct looking frame seems to have the same width on the bottom as it does on the sides. – paxdiablo Jul 07 '10 at 06:25
  • So you're saying there are 30 pixels _above_ the client area and 8 on the bottom and sides? – paxdiablo Jul 07 '10 at 06:45
  • The second image illustrates the border of the window and the content (pink) perfectly. By my measurements, the edge between the end of the window and the end of the center content is 8 pixels on the left, right, and bottom, and 30 pixels at the top. – Devin Jeanpierre Jul 07 '10 at 06:50
  • I would actually print out the information you get back from GetWindowRect then use a tool like HyperSnap to work out if the co-ordinates are correct (we use that at work and it allows to to easily find the corners). It seems to me that either GetWindowsRect is lying (unlikely) or ImageGrab.grab(bbox) is deficient somehow. Personally, I'd be betting on the latter. Once you've established where the problem lies, we can think about alternatives (it's a pity the snipping tool doesn't have a cmdline interface to just grab the current window). – paxdiablo Jul 07 '10 at 06:53
  • One of the possible alternatives is to use GetWindowRect to get the whole thing then use GetClientRec to get the client area. You should be able to work out the four borders from that on a per Window basis. – paxdiablo Jul 07 '10 at 06:57
  • I already established which was lying, and it's GetWindowsRect. When I saw the outcome of the crop, I tested it myself manually. This is the origin of my statement, "the crop operates correctly". – Devin Jeanpierre Jul 07 '10 at 07:00
  • To clarify: the dimensions returned by GetWindowsRect are a window that is 306 wide and 328 tall (e.g. (175, 175, 481, 503)). This matches perfectly with the crop (and is even positioned correctly), but is smaller than the "Snipping Tool" crop (and cuts off the corners etc.) – Devin Jeanpierre Jul 07 '10 at 07:16
  • @Devin, see my final update. It may be that PIL is not linked with WINVER=6 and thus may be getting the incorrect values. It's worth testing the proposed solution to see if it gives you the correct values. – paxdiablo Jul 07 '10 at 07:28
  • Sorry it took so long to check up, I was procrastinating on actually testing because ctypes is a pain. It works perfectly! – Devin Jeanpierre Jul 07 '10 at 20:32
4

I know it is a bit old topic. But it took quite a bit of searching and I went through the ctypes pain myself to get paxdiablo's solution working in Python. Just wanted to share working code sample for wxPython:

try:
    f = ctypes.windll.dwmapi.DwmGetWindowAttribute
except WindowsError:
    f = None
if f: # Vista & 7 stuff
    rect = ctypes.wintypes.RECT()
    DWMWA_EXTENDED_FRAME_BOUNDS = 9
    f(ctypes.wintypes.HWND(self.GetHandle()),
      ctypes.wintypes.DWORD(DWMWA_EXTENDED_FRAME_BOUNDS),
      ctypes.byref(rect),
      ctypes.sizeof(rect)
      )
    size = (rect.right - rect.left, rect.bottom - rect.top)        
else:      
    size = self.GetSize()
Fenikso
  • 9,251
  • 5
  • 44
  • 72
2

GetWindowRect on Windows 7 appears to not include the right and bottom window frame edges (at least with the Aero theme afaik), if the window was created without the WS_SIZEBOX style (i.e, you want a non-sizable window).

The problem is, WS_SIZEBOX is the same as WS_THICKFRAME, and on Aero, windows have the thickframe whether they can be resized or not. But the GetWindowRect function thinks that a non-resizable window is thinner.

The fix? You can create the window with WS_SIZEBOX, call GetWindowRect, then use SetWindowLongPtr(GWL_STYLE, ...) to turn off WS_SIZEBOX, but this will create an ugly white border inside the client area.

Instead, leave WS_SIZEBOX enabled, and simply return the same value for ptMinTrackSize and ptMaxTraceSize in the MINMAXINFO structure when responding to the WM_GETMINMAXINFO message. This will keep the window from being resizable and GetWindowRect will return proper data. The only downside is that the mouse cursor will still change to a resizing cursor when the mouse pointer goes over the window frame, but it's the lesser of the evils so far.

1

DwmGetWindowAttribute

http://msdn.microsoft.com/en-us/library/aa969515%28VS.85%29.aspx

Windows programmer
  • 7,871
  • 1
  • 22
  • 23
1

first, you call GetClientRect to retrive the client rectangle R1, then call AdjustWindowRectEx to calculate the accurate bounds according to R1.

JGShining
  • 11
  • 1
0

GetWindowRect return correct values, but for explicit handle to the window. Use GetParent function to get handle of the parent window, while GetWindoWRect return for you maximum RECT or GetParent return value is NULL.

Survarium
  • 190
  • 2
  • 6