From 9c7724e6feede4b04d7f410401fab9b6e28855ca Mon Sep 17 00:00:00 2001 From: Alan Kilborn <77065706+alankilborn@users.noreply.github.com> Date: Fri, 30 May 2025 07:56:48 -0400 Subject: [PATCH] Add support for file-save dialog --- helper/WinDialog/__init__.py | 4 +- .../WinDialog/__tests__/test_com_dialogs.py | 48 ++++++++-- helper/WinDialog/com_dialogs/__init__.py | 91 ++++++++++++------- 3 files changed, 99 insertions(+), 44 deletions(-) diff --git a/helper/WinDialog/__init__.py b/helper/WinDialog/__init__.py index 8b3b604..b5a6dae 100644 --- a/helper/WinDialog/__init__.py +++ b/helper/WinDialog/__init__.py @@ -58,7 +58,7 @@ 'Button', 'DefaultButton', 'CheckBoxButton', 'GroupBox', 'CommandButton', 'RadioButton', 'RadioPushButton', 'SplitButton', 'Label', 'TruncatedLabel', 'BlackFramedLabel', 'CenteredLabel', 'RigthAlignedLabel', 'ComboBox', 'ComboBoxEx', 'ListBox', 'TextBox', 'ListView', 'ProgressBar', 'StatusBar', 'UpDown', 'TreeView', - 'DirectoryPicker', 'FileOpenDialog' + 'DirectoryPicker', 'FileOpenDialog', 'FileSaveDialog' ] __version__ = '0.3' __author__ = 'ekopalypse' @@ -102,7 +102,7 @@ # from .controls.systabcontrol32 import ... # from .controls.toolbarwindow32 import ... -from .com_dialogs import FileOpenDialog, DirectoryPicker +from .com_dialogs import FileOpenDialog, DirectoryPicker, FileSaveDialog from .resource_parser import parser diff --git a/helper/WinDialog/__tests__/test_com_dialogs.py b/helper/WinDialog/__tests__/test_com_dialogs.py index 693afe4..5256ac7 100644 --- a/helper/WinDialog/__tests__/test_com_dialogs.py +++ b/helper/WinDialog/__tests__/test_com_dialogs.py @@ -1,13 +1,43 @@ -from WinDialog import FileOpenDialog, DirectoryPicker +from WinDialog import FileOpenDialog, DirectoryPicker, FileSaveDialog from WinDialog.com_dialogs import FOS -dlg = FileOpenDialog() -dlg.setOptions(FOS.ALLOWMULTISELECT) -print(dlg.show()) +def test_dlgs(): -print(DirectoryPicker().show()) + answer = notepad.messageBox('About to demo a file-open dialog where you can select multiple items -- proceed?', '', MESSAGEBOXFLAGS.YESNOCANCEL) + if answer == MESSAGEBOXFLAGS.RESULTCANCEL: return + if answer == MESSAGEBOXFLAGS.RESULTYES: + open_dlg = FileOpenDialog() + open_dlg.setOptions(FOS.ALLOWMULTISELECT) + result = open_dlg.show() + if len(result) > 0: notepad.messageBox(str(result), 'Result') -options = dlg.getOptions() -options &= ~FOS.ALLOWMULTISELECT -dlg.setOptions(options) -print(dlg.show()) \ No newline at end of file + answer = notepad.messageBox('About to demo a file-open dialog where you can only select 1 item -- proceed?', '', MESSAGEBOXFLAGS.YESNOCANCEL) + if answer == MESSAGEBOXFLAGS.RESULTCANCEL: return + if answer == MESSAGEBOXFLAGS.RESULTYES: + open_dlg = FileOpenDialog() + open_options = open_dlg.getOptions() + open_options &= ~FOS.ALLOWMULTISELECT + open_dlg.setOptions(open_options) + result = open_dlg.show() + if len(result) > 0: notepad.messageBox(str(result), 'Result') + + answer = notepad.messageBox('About to demo a file-save dialog -- proceed?', '', MESSAGEBOXFLAGS.YESNOCANCEL) + if answer == MESSAGEBOXFLAGS.RESULTCANCEL: return + if answer == MESSAGEBOXFLAGS.RESULTYES: + save_dlg = FileSaveDialog() + save_dlg.setFolder(r'c:') + save_dlg.setFileTypes([['All files', '*.*'], ['Text Files', '*.txt'], ['Log Files', '*.log']]) + save_options = save_dlg.getOptions() + save_options |= FOS.OVERWRITEPROMPT + save_dlg.setOptions(save_options) + result = save_dlg.show() + if len(result) > 0: notepad.messageBox(str(result), 'Result') + + answer = notepad.messageBox('About to demo a directory-picker dialog -- proceed?', '', MESSAGEBOXFLAGS.YESNOCANCEL) + if answer == MESSAGEBOXFLAGS.RESULTCANCEL: return + if answer == MESSAGEBOXFLAGS.RESULTYES: + dir_pick_dlg = DirectoryPicker() + result = dir_pick_dlg.show() + if len(result) > 0: notepad.messageBox(str(result), 'Result') + +test_dlgs() diff --git a/helper/WinDialog/com_dialogs/__init__.py b/helper/WinDialog/com_dialogs/__init__.py index 053aced..31cf44f 100644 --- a/helper/WinDialog/com_dialogs/__init__.py +++ b/helper/WinDialog/com_dialogs/__init__.py @@ -192,7 +192,7 @@ def __post_init__(self, rclsid, riid, extend_vtable): self._set_title = WINFUNCTYPE(HRESULT, c_void_p, LPCWSTR)(self._vtable[17]) self._set_ok_button_label = WINFUNCTYPE(HRESULT, c_void_p, LPCWSTR)(self._vtable[18]) self._set_file_name_label = WINFUNCTYPE(HRESULT, c_void_p, LPCWSTR)(self._vtable[19]) - # self._get_result = WINFUNCTYPE(HRESULT, c_void_p, POINTER(c_void_p))(self._vtable[20]) + self._get_result = WINFUNCTYPE(HRESULT, c_void_p, POINTER(c_void_p))(self._vtable[20]) self._add_place = WINFUNCTYPE(HRESULT, c_void_p, c_void_p, UINT)(self._vtable[21]) self._set_default_extension = WINFUNCTYPE(HRESULT, c_void_p, LPCWSTR)(self._vtable[22]) # self._close = WINFUNCTYPE(HRESULT, c_void_p, HRESULT)(self._vtable[23]) @@ -492,43 +492,68 @@ def setOptions(self, options): self._set_options(self.this, default_options.value | options) -# @dataclass -# class FileSaveDialog(FileDialog): - # # https://learn.microsoft.com/en-us/windows/win32/api/shobjidl_core/nn-shobjidl_core-ifilesavedialog - # ''' - # FileSaveDialog Class +@dataclass +class FileSaveDialog(FileDialog): + # https://learn.microsoft.com/en-us/windows/win32/api/shobjidl_core/nn-shobjidl_core-ifilesavedialog + ''' + FileSaveDialog Class - # Represents the standard COM-based file open dialog window. + Represents the standard COM-based file save dialog window. - # The FileSaveDialog class provides a template for creating and managing dialog windows. - # It encapsulates properties and behaviors common to dialog windows, such as - # title, size, position, styles, and controls. + The FileSaveDialog class provides a template for creating and managing dialog windows. + It encapsulates properties and behaviors common to dialog windows, such as + title, size, position, styles, and controls. - # Attributes: - # title (str): The title of the dialog window. - # parent (int): The handle of the parent window for the dialog. - # ''' + Attributes: + title (str): The title of the dialog window. + parent (int): The handle of the parent window for the dialog. + ''' - # def __post_init__(self): - # """ - # Perform post-initialization tasks for the FileSaveDialog object. + selectedItem: List = field(default_factory=list) + parent: int = notepad.hwnd - # This method is automatically called after the initialization of the FileSaveDialog object. - # It creates the com object and its vtable (object methods). + def __post_init__(self): + """ + Perform post-initialization tasks for the FileSaveDialog object. - # Args: - # None. + This method is automatically called after the initialization of the FileSaveDialog object. + It creates the com object and its vtable (object methods). - # Returns: - # None. - # """ - # super().__post_init__(GUID('C0B4E2F3-BA21-4773-8DBA-335EC946EB8B'), GUID('84BCCD23-5FDE-4CDB-AEA4-AF64B83D78AB'), 5) + Args: + None. - # def show(self, hwnd=None): - # try: - # self._show(self.this, hwnd) - # except OSError as e: - # if len(e.args) > 3 and e.args[3] == -2147023673: - # pass # User cancelled dialog - # else: - # raise + Returns: + None. + """ + super().__post_init__(GUID('C0B4E2F3-BA21-4773-8DBA-335EC946EB8B'), GUID('84BCCD23-5FDE-4CDB-AEA4-AF64B83D78AB'), 5) + # self._set_saveas_item = WINFUNCTYPE(HRESULT, c_void_p, POINTER(c_void_p))(self._vtable[27]) + # self._set_properties = WINFUNCTYPE(HRESULT, c_void_p, POINTER(c_void_p))(self._vtable[28]) + # self._set_collected_properties = WINFUNCTYPE(HRESULT, c_void_p, POINTER(c_void_p))(self._vtable[29]) + # self._get_properties = WINFUNCTYPE(HRESULT, c_void_p, POINTER(c_void_p))(self._vtable[30]) + # self._apply_properties = WINFUNCTYPE(HRESULT, c_void_p, POINTER(c_void_p))(self._vtable[30]) + + def show(self, hwnd=None): + ''' + This method displays the dialog on the screen. + The method blocks until the dialog is closed. + + Args: + None + + Returns: + list: A single-element list containing the selected item (or empty list if cancelled or some error) + ''' + try: + self.selectedItem.clear() + self._show(self.this, self.parent) + ishell_object = c_void_p() + if self._get_result(self.this, byref(ishell_object)) == 0: + ishell_item = IShellItem(ishell_object) + self.selectedItem.append(ishell_item.get_display_name()) + ishell_item.release() + except OSError as e: + if len(e.args) > 3 and e.args[3] == -2147023673: # see, e.g., https://knowledge.broadcom.com/external/article/152212/error-codes-list-for-microsoft-technolog.html + pass # User cancelled dialog + else: + raise + return self.selectedItem