1+ import argparse
2+ import ctypes
3+ import re
4+ import xml .etree .ElementTree as ET
5+ from ctypes import wintypes
6+
7+ # usage: winFullScreenConfig.py [display_selector xml_file]
8+ # If no arguments are given, prints all display geometries.
9+ # Otherwise, updates the given config XML file with the geometry of the selected display.
10+ # The display selector can be a numeric display ID or one of: primary, secondary, prefer-secondary.
11+ # The script uses Windows API calls to query display information and modify the XML config accordingly.
12+
13+ DISPLAY_DEVICE_ATTACHED_TO_DESKTOP = 0x00000001
14+ DISPLAY_DEVICE_PRIMARY_DEVICE = 0x00000004
15+ ENUM_CURRENT_SETTINGS = - 1
16+ CCHDEVICENAME = 32
17+ CCHFORMNAME = 32
18+
19+
20+ class POINTL (ctypes .Structure ):
21+ _fields_ = [
22+ ("x" , wintypes .LONG ),
23+ ("y" , wintypes .LONG ),
24+ ]
25+
26+
27+ class DISPLAY_DEVICEW (ctypes .Structure ):
28+ _fields_ = [
29+ ("cb" , wintypes .DWORD ),
30+ ("DeviceName" , wintypes .WCHAR * 32 ),
31+ ("DeviceString" , wintypes .WCHAR * 128 ),
32+ ("StateFlags" , wintypes .DWORD ),
33+ ("DeviceID" , wintypes .WCHAR * 128 ),
34+ ("DeviceKey" , wintypes .WCHAR * 128 ),
35+ ]
36+
37+
38+ class DEVMODEW (ctypes .Structure ):
39+ _fields_ = [
40+ ("dmDeviceName" , wintypes .WCHAR * CCHDEVICENAME ),
41+ ("dmSpecVersion" , wintypes .WORD ),
42+ ("dmDriverVersion" , wintypes .WORD ),
43+ ("dmSize" , wintypes .WORD ),
44+ ("dmDriverExtra" , wintypes .WORD ),
45+ ("dmFields" , wintypes .DWORD ),
46+ ("dmPosition" , POINTL ),
47+ ("dmDisplayOrientation" , wintypes .DWORD ),
48+ ("dmDisplayFixedOutput" , wintypes .DWORD ),
49+ ("dmColor" , wintypes .SHORT ),
50+ ("dmDuplex" , wintypes .SHORT ),
51+ ("dmYResolution" , wintypes .SHORT ),
52+ ("dmTTOption" , wintypes .SHORT ),
53+ ("dmCollate" , wintypes .SHORT ),
54+ ("dmFormName" , wintypes .WCHAR * CCHFORMNAME ),
55+ ("dmLogPixels" , wintypes .WORD ),
56+ ("dmBitsPerPel" , wintypes .DWORD ),
57+ ("dmPelsWidth" , wintypes .DWORD ),
58+ ("dmPelsHeight" , wintypes .DWORD ),
59+ ("dmDisplayFlags" , wintypes .DWORD ),
60+ ("dmDisplayFrequency" , wintypes .DWORD ),
61+ ("dmICMMethod" , wintypes .DWORD ),
62+ ("dmICMIntent" , wintypes .DWORD ),
63+ ("dmMediaType" , wintypes .DWORD ),
64+ ("dmDitherType" , wintypes .DWORD ),
65+ ("dmReserved1" , wintypes .DWORD ),
66+ ("dmReserved2" , wintypes .DWORD ),
67+ ("dmPanningWidth" , wintypes .DWORD ),
68+ ("dmPanningHeight" , wintypes .DWORD ),
69+ ]
70+
71+
72+ def get_display_id (device_name ):
73+ match = re .search (r"DISPLAY(\d+)$" , device_name )
74+ if match :
75+ return int (match .group (1 ))
76+ return None
77+
78+
79+ def get_displays ():
80+ displays = []
81+ index = 0
82+
83+ while True :
84+ device = DISPLAY_DEVICEW ()
85+ device .cb = ctypes .sizeof (DISPLAY_DEVICEW )
86+ if not ctypes .windll .user32 .EnumDisplayDevicesW (None , index , ctypes .byref (device ), 0 ):
87+ break
88+
89+ if not (device .StateFlags & DISPLAY_DEVICE_ATTACHED_TO_DESKTOP ):
90+ index += 1
91+ continue
92+
93+ devmode = DEVMODEW ()
94+ devmode .dmSize = ctypes .sizeof (DEVMODEW )
95+ if ctypes .windll .user32 .EnumDisplaySettingsW (device .DeviceName , ENUM_CURRENT_SETTINGS , ctypes .byref (devmode )):
96+ displays .append ({
97+ "display_id" : get_display_id (device .DeviceName ),
98+ "name" : device .DeviceName ,
99+ "label" : device .DeviceString ,
100+ "left" : devmode .dmPosition .x ,
101+ "top" : devmode .dmPosition .y ,
102+ "width" : devmode .dmPelsWidth ,
103+ "height" : devmode .dmPelsHeight ,
104+ "right" : devmode .dmPosition .x + devmode .dmPelsWidth ,
105+ "bottom" : devmode .dmPosition .y + devmode .dmPelsHeight ,
106+ "primary" : bool (device .StateFlags & DISPLAY_DEVICE_PRIMARY_DEVICE ),
107+ })
108+
109+ index += 1
110+
111+ return displays
112+
113+
114+ def find_display (displays , display_id ):
115+ for display in displays :
116+ if display ["display_id" ] == display_id :
117+ return display
118+ return None
119+
120+
121+ def resolve_display (displays , selector ):
122+ normalized = selector .strip ().lower ()
123+
124+ if normalized == "primary" :
125+ for display in displays :
126+ if display ["primary" ]:
127+ return display
128+ return None
129+
130+ if normalized == "secondary" :
131+ secondary_displays = [display for display in displays if not display ["primary" ]]
132+ if len (secondary_displays ) == 1 :
133+ return secondary_displays [0 ]
134+ if not secondary_displays :
135+ return None
136+ raise SystemExit ("More than one secondary monitor found. Use the numeric display ID instead." )
137+
138+ if normalized == "prefer-secondary" :
139+ secondary_displays = [display for display in displays if not display ["primary" ]]
140+ if len (secondary_displays ) == 1 :
141+ return secondary_displays [0 ]
142+ if len (secondary_displays ) > 1 :
143+ raise SystemExit ("More than one secondary monitor found. Use the numeric display ID instead." )
144+
145+ for display in displays :
146+ if display ["primary" ]:
147+ return display
148+ return None
149+
150+ try :
151+ display_id = int (selector )
152+ except ValueError as error :
153+ raise SystemExit (
154+ "Display selector must be a numeric display ID or one of: primary, secondary, prefer-secondary."
155+ ) from error
156+
157+ return find_display (displays , display_id )
158+
159+
160+ def update_window_config (xml_path , display ):
161+ tree = ET .parse (xml_path )
162+ root = tree .getroot ()
163+ window = root .find (".//WindowConfig/Window" )
164+ if window is None :
165+ raise ValueError ("No <Window> element found inside <WindowConfig>." )
166+
167+ window .set ("width" , str (display ["width" ]))
168+ window .set ("height" , str (display ["height" ]))
169+ window .set ("left" , str (display ["left" ]))
170+ window .set ("top" , str (display ["top" ]))
171+
172+ tree .write (xml_path , encoding = "utf-8" , xml_declaration = True )
173+
174+
175+ def print_displays (displays , primary ):
176+ selected = [display for display in displays if display ["primary" ] is primary ]
177+ if not selected :
178+ print ("No primary monitor found." if primary else "No secondary monitor found." )
179+ return
180+
181+ for index , display in enumerate (selected , 1 ):
182+ if primary :
183+ label = f"Primary monitor { index } " if len (selected ) > 1 else "Primary monitor"
184+ else :
185+ label = f"Secondary monitor { index } " if len (selected ) > 1 else "Secondary monitor"
186+
187+ print (f"{ label } :" )
188+ if display ["display_id" ] is not None :
189+ print (f" Display : { display ['display_id' ]} " )
190+ print (f" Device : { display ['name' ]} ({ display ['label' ]} )" )
191+ print (f" Position : ({ display ['left' ]} , { display ['top' ]} )" )
192+ print (f" Size : { display ['width' ]} x { display ['height' ]} pixels" )
193+ print (
194+ f" Rect : left={ display ['left' ]} , top={ display ['top' ]} , "
195+ f"right={ display ['right' ]} , bottom={ display ['bottom' ]} "
196+ )
197+
198+
199+ def parse_args ():
200+ parser = argparse .ArgumentParser (
201+ description = "Print Windows display geometry or apply a display geometry to a COVISE XML config."
202+ )
203+ parser .add_argument (
204+ "display_selector" ,
205+ nargs = "?" ,
206+ help = "Windows display number or one of: primary, secondary, prefer-secondary" ,
207+ )
208+ parser .add_argument ("xml_file" , nargs = "?" , help = "Path to the XML config file to modify" )
209+ return parser .parse_args ()
210+
211+
212+ def main ():
213+ args = parse_args ()
214+ all_displays = get_displays ()
215+
216+ if args .display_selector is None and args .xml_file is None :
217+ print_displays (all_displays , primary = False )
218+ print_displays (all_displays , primary = True )
219+ return
220+
221+ if args .display_selector is None or args .xml_file is None :
222+ raise SystemExit ("Provide both display_selector and xml_file, or provide neither." )
223+
224+ display = resolve_display (all_displays , args .display_selector )
225+ if display is None :
226+ raise SystemExit (f"Display { args .display_selector } not found." )
227+
228+ update_window_config (args .xml_file , display )
229+ print (f"Updated { args .xml_file } for display { args .display_selector } ." )
230+ print (f" Position : ({ display ['left' ]} , { display ['top' ]} )" )
231+ print (f" Size : { display ['width' ]} x { display ['height' ]} pixels" )
232+
233+
234+ if __name__ == "__main__" :
235+ main ()
0 commit comments