1515 c_void_p ,
1616 cast ,
1717 create_string_buffer ,
18+ create_unicode_buffer ,
1819 get_last_error ,
1920 sizeof ,
2021 string_at ,
22+ wstring_at
2123)
2224from ctypes import wintypes as w
2325from pathlib import Path
@@ -144,6 +146,37 @@ class ITEMIDLIST(Structure):
144146 _fields_ = (("mkid" , SHITEMID ),)
145147
146148
149+ class OPENFILENAME (Structure ):
150+ _fields_ = (
151+ ("lStructSize" , w .DWORD ),
152+ ("hwndOwner" , w .HWND ),
153+ ("hInstance" , w .HINSTANCE ),
154+ ("lpstrFilter" , w .LPWSTR ),
155+ ("lpstrCustomFilter" , w .LPWSTR ),
156+ ("nMaxCustFilter" , w .DWORD ),
157+ ("nFilterIndex" , w .DWORD ),
158+ ("lpstrFile" , w .LPWSTR ),
159+ ("nMaxFile" , w .DWORD ),
160+ ("lpstrFileTitle" , w .LPWSTR ),
161+ ("nMaxFileTitle" , w .DWORD ),
162+ ("lpstrInitialDir" , w .LPWSTR ),
163+ ("lpstrTitle" , w .LPWSTR ),
164+ ("Flags" , w .DWORD ),
165+ ("nFileOffset" , w .WORD ),
166+ ("nFileExtension" , w .WORD ),
167+ ("lpstrDefExt" , w .LPWSTR ),
168+ ("lCustData" , w .LPARAM ),
169+ ("lpfnHook" , w .LPVOID ),
170+ ("lpTemplateName" , w .LPWSTR ),
171+ ("pvReserved" , w .LPVOID ),
172+ ("dwReserved" , w .DWORD ),
173+ ("FlagsEx" , w .DWORD ),
174+ )
175+
176+ comdlg32 = WinDLL ("comdlg32" , use_last_error = True )
177+ comdlg32 .GetOpenFileNameW .argtypes = (POINTER (OPENFILENAME ),)
178+ comdlg32 .GetOpenFileNameW .restype = w .BOOL
179+
147180kernel32 = WinDLL ("kernel32" , use_last_error = True )
148181kernel32 .GetModuleHandleW .argtypes = (w .LPCWSTR ,)
149182kernel32 .GetModuleHandleW .restype = w .HMODULE
@@ -244,13 +277,16 @@ class Win32(GUI):
244277
245278 start_button = None
246279 choose_folder_button = None
280+ choose_files_button = None
247281
248282 input_field = None
249283 checkbox = None
250284 reveal_text = None
251285 label = None
252- info = None
253286 upload_label = None
287+ info = None
288+ file_info = None
289+ file_label = None
254290 progress_bar = None
255291 image = None
256292
@@ -293,6 +329,14 @@ def message(self, message: str) -> None:
293329 def choose_folder (self ) -> None :
294330 if self ._closed :
295331 return
332+
333+ if self .folder :
334+ self .folder = None
335+ user32 .SetWindowTextA (self .label , b"No path selected..." )
336+ user32 .SetWindowTextA (self .choose_folder_button , b"Choose folder" )
337+ if not self .files :
338+ user32 .EnableWindow (self .start_button , False )
339+ return
296340
297341 browseinfo = BROWSEINFOA ()
298342 browseinfo .hwndOwner = self .hwnd
@@ -310,11 +354,54 @@ def choose_folder(self) -> None:
310354 if pathstr :
311355 self .folder = Path (pathstr )
312356 user32 .SetWindowTextA (self .label , string_at (path ))
357+ user32 .SetWindowTextA (self .choose_folder_button , b"Clear folder" )
313358 user32 .EnableWindow (self .start_button , True )
314359
315360 # Caller is responsible for freeing this memory.
316361 ole32 .CoTaskMemFree (choice )
317362
363+ def choose_files (self ) -> None :
364+ if self ._closed :
365+ return
366+
367+ if self .files :
368+ self .files = None
369+ user32 .SetWindowTextA (self .file_label , b"No file(s) selected..." )
370+ user32 .SetWindowTextA (self .choose_files_button , b"Choose files" )
371+ if not self .folder :
372+ user32 .EnableWindow (self .start_button , False )
373+ return
374+
375+ buffer_size = 4096
376+ buffer = create_unicode_buffer (buffer_size )
377+
378+ ofn = OPENFILENAME ()
379+ ofn .lStructSize = sizeof (OPENFILENAME )
380+ ofn .hwndOwner = self .hwnd
381+ ofn .lpstrFile = cast (buffer , w .LPWSTR )
382+ ofn .nMaxFile = sizeof (buffer )
383+ ofn .lpstrFilter = "All Files\0 *.*\0 Text Files\0 *.txt\0 \0 "
384+ ofn .nFilterIndex = 1
385+ ofn .Flags = 0x00002000 | 0x00080000 | 0x00000200
386+
387+ if comdlg32 .GetOpenFileNameW (byref (ofn )):
388+ raw_data = string_at (buffer , buffer_size * 2 ).decode ("utf-16le" ).strip ("\0 " )
389+ parts = raw_data .split ("\0 " )
390+
391+ if len (parts ) > 1 :
392+ dir_path , * file_list = parts
393+ selected_paths = [f"{ dir_path } \\ { file } " for file in file_list ]
394+ else :
395+ selected_paths = [parts [0 ]]
396+
397+ if selected_paths :
398+ self .files = [path for path in selected_paths if path ]
399+ user32 .SetWindowTextA (self .file_label , f"{ len (self .files )} file(s) selected" .encode ("utf-8" ))
400+ user32 .SetWindowTextA (self .choose_files_button , b"Clear file(s)" )
401+ user32 .EnableWindow (self .start_button , True )
402+ else :
403+ print ("User cancelled file selection" )
404+
318405 def show (self ) -> None :
319406 if self ._closed :
320407 return
@@ -389,31 +476,48 @@ def show(self) -> None:
389476 0 , "static" , "Acquire output folder:" , WS_CHILD | WS_VISIBLE , 20 , 20 , 200 , 20 , hwnd , 0 , 0 , 0
390477 )
391478 self .label = user32 .CreateWindowExW (
392- 0 , "static" , "No path selected..." , WS_CHILD | WS_VISIBLE , 20 , 40 , 400 , 25 , hwnd , 0 , 0 , 0
479+ 0 , "static" , "No path selected..." , WS_CHILD | WS_VISIBLE , 20 , 40 , 300 , 25 , hwnd , 0 , 0 , 0
480+ )
481+
482+ self .file_info = user32 .CreateWindowExW (
483+ 0 , "static" , "Select files(s) to collect:" , WS_CHILD | WS_VISIBLE , 20 , 130 , 250 , 20 , hwnd , 0 , 0 , 0
393484 )
485+
394486 self .choose_folder_button = user32 .CreateWindowExW (
395487 0 , "Button" , "Choose folder" , WS_CHILD | WS_VISIBLE | WS_BORDER | BS_FLAT , 450 , 35 , 120 , 32 , hwnd , 0 , 0 , 0
396488 )
489+ self .choose_files_button = user32 .CreateWindowExW (
490+ 0 , "Button" , "Choose files" , WS_CHILD | WS_VISIBLE | WS_BORDER | BS_FLAT , 450 , 145 , 120 , 32 , hwnd , 0 , 0 , 0
491+ )
492+
493+ self .file_label = user32 .CreateWindowExW (
494+ 0 , "static" , "No file(s) selected..." , WS_CHILD | WS_VISIBLE , 20 , 150 , 300 , 25 , hwnd , 0 , 0 , 0
495+ )
496+
397497 self .start_button = user32 .CreateWindowExW (
398498 0 ,
399499 "Button" ,
400500 "Start" ,
401501 WS_CHILD | WS_VISIBLE | WS_BORDER | WS_DISABLED | BS_FLAT ,
402502 250 ,
403- 100 ,
503+ 250 ,
404504 100 ,
405505 32 ,
406506 hwnd ,
407507 0 ,
408508 0 ,
409509 0 ,
410510 )
511+
411512 if hFont :
412513 SendMessage (self .info , WM_SETFONT , hFont , 1 )
514+ SendMessage (self .file_info , WM_SETFONT , hFont , 1 )
413515 SendMessage (self .start_button , WM_SETFONT , hFont , 1 )
414516 SendMessage (self .choose_folder_button , WM_SETFONT , hFont , 1 )
517+ SendMessage (self .choose_files_button , WM_SETFONT , hFont , 1 )
415518 SendMessage (self .label , WM_SETFONT , hFont , 1 )
416-
519+ SendMessage (self .file_label , WM_SETFONT , hFont , 1 )
520+
417521 msg = w .MSG ()
418522 while user32 .GetMessageW (byref (msg ), None , 0 , 0 ) != 0 :
419523 user32 .TranslateMessage (byref (msg ))
@@ -427,6 +531,10 @@ def _message(self, hwnd: w.HWND, message: w.UINT, wParam: w.WPARAM, lParam: w.LP
427531 event = HIWORD (wParam )
428532 if event == BN_CLICKED :
429533 self .choose_folder ()
534+ elif lParam == self .choose_files_button :
535+ event = HIWORD (wParam )
536+ if event == BN_CLICKED :
537+ self .choose_files ()
430538 elif lParam == self .start_button :
431539 user32 .EnableWindow (self .start_button , False )
432540 user32 .EnableWindow (self .choose_folder_button , False )
@@ -447,7 +555,7 @@ def _message(self, hwnd: w.HWND, message: w.UINT, wParam: w.WPARAM, lParam: w.LP
447555 user32 .DestroyWindow (hwnd )
448556 return 0
449557
450- if message == WM_CTLCOLORSTATIC and lParam in [self .upload_label , self .info ]:
558+ if message == WM_CTLCOLORSTATIC and lParam in [self .upload_label , self .info , self . file_info ]:
451559 return gdi32 .GetStockObject (WHITE_BRUSH )
452560
453561 if message == WM_DESTROY :
0 commit comments