├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE ├── README.md ├── _examples ├── notusingutil │ ├── OpenFile.go │ ├── OpenMultipleFiles.go │ ├── PickFolder.go │ └── SaveFile.go └── usingutil │ ├── OpenFile.go │ ├── OpenMultipleFiles.go │ ├── PickFolder.go │ └── SaveFile.go ├── cfd ├── CommonFileDialog.go ├── CommonFileDialog_nonWindows.go ├── CommonFileDialog_windows.go ├── DialogConfig.go ├── errors.go ├── iFileOpenDialog.go ├── iFileSaveDialog.go ├── iShellItem.go ├── iShellItemArray.go ├── vtblCommon.go └── vtblCommonFunc.go ├── cfdutil └── CFDUtil.go ├── go.mod ├── go.sum └── util ├── util.go └── util_test.go /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: harry1453 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # GoLand 15 | .idea/ 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Harry Phillips 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Common File Dialog bindings for Golang 2 | 3 | This library contains bindings for Windows Vista and newer's [Common File Dialogs](https://docs.microsoft.com/en-us/windows/win32/shell/common-file-dialog), which is the standard system dialog for selecting files or folders to open or save. 4 | 5 | The Common File Dialogs have to be accessed via the [COM Interface](https://en.wikipedia.org/wiki/Component_Object_Model), normally via C++ or via bindings (like in C#). 6 | 7 | This library contains bindings for Golang. **It does not require CGO**, and contains empty stubs for non-windows platforms (so is safe to compile and run on platforms other than windows, but will just return errors at runtime). 8 | 9 | This can be very useful if you want to quickly get a file selector in your Golang application. The `cfdutil` package contains utility functions with a single call to open and configure a dialog, and then get the result from it. Examples for this are in [`_examples/usingutil`](_examples/usingutil). Or, if you want finer control over the dialog's operation, you can use the base package. Examples for this are in [`_examples/notusingutil`](_examples/notusingutil). 10 | 11 | This library is available under the MIT license. 12 | 13 | Currently supported features: 14 | * Open File Dialog (to open a single file) 15 | * Open Multiple Files Dialog (to open multiple files) 16 | * Open Folder Dialog 17 | * Save File Dialog 18 | * Dialog "roles" to allow Windows to remember different "last locations" for different types of dialog 19 | * Set dialog Title, Default Folder and Initial Folder 20 | * Set dialog File Filters 21 | -------------------------------------------------------------------------------- /_examples/notusingutil/OpenFile.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/harry1453/go-common-file-dialog/cfd" 5 | "log" 6 | "time" 7 | ) 8 | 9 | func main() { 10 | openDialog, err := cfd.NewOpenFileDialog(cfd.DialogConfig{ 11 | Title: "Open A File", 12 | Role: "OpenFileExample", 13 | FileFilters: []cfd.FileFilter{ 14 | { 15 | DisplayName: "Text Files (*.txt)", 16 | Pattern: "*.txt", 17 | }, 18 | { 19 | DisplayName: "Image Files (*.jpg, *.png)", 20 | Pattern: "*.jpg;*.png", 21 | }, 22 | { 23 | DisplayName: "All Files (*.*)", 24 | Pattern: "*.*", 25 | }, 26 | }, 27 | SelectedFileFilterIndex: 2, 28 | FileName: "file.txt", 29 | DefaultExtension: "txt", 30 | }) 31 | if err != nil { 32 | log.Fatal(err) 33 | } 34 | go func() { 35 | time.Sleep(2 * time.Second) 36 | if err := openDialog.SetFileName("hello world"); err != nil { 37 | log.Fatal(err) 38 | } 39 | }() 40 | if err := openDialog.Show(); err != nil { 41 | log.Fatal(err) 42 | } 43 | result, err := openDialog.GetResult() 44 | if err == cfd.ErrorCancelled { 45 | log.Fatal("Dialog was cancelled by the user.") 46 | } else if err != nil { 47 | log.Fatal(err) 48 | } 49 | log.Printf("Chosen file: %s\n", result) 50 | } 51 | -------------------------------------------------------------------------------- /_examples/notusingutil/OpenMultipleFiles.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/harry1453/go-common-file-dialog/cfd" 5 | "log" 6 | ) 7 | 8 | func main() { 9 | openMultiDialog, err := cfd.NewOpenMultipleFilesDialog(cfd.DialogConfig{ 10 | Title: "Open Multiple Files", 11 | Role: "OpenFilesExample", 12 | FileFilters: []cfd.FileFilter{ 13 | { 14 | DisplayName: "Text Files (*.txt)", 15 | Pattern: "*.txt", 16 | }, 17 | { 18 | DisplayName: "Image Files (*.jpg, *.png)", 19 | Pattern: "*.jpg;*.png", 20 | }, 21 | { 22 | DisplayName: "All Files (*.*)", 23 | Pattern: "*.*", 24 | }, 25 | }, 26 | SelectedFileFilterIndex: 2, 27 | FileName: "file.txt", 28 | DefaultExtension: "txt", 29 | }) 30 | if err != nil { 31 | log.Fatal(err) 32 | } 33 | if err := openMultiDialog.Show(); err != nil { 34 | log.Fatal(err) 35 | } 36 | results, err := openMultiDialog.GetResults() 37 | if err == cfd.ErrorCancelled { 38 | log.Fatal("Dialog was cancelled by the user.") 39 | } else if err != nil { 40 | log.Fatal(err) 41 | } 42 | log.Printf("Chosen file(s): %s\n", results) 43 | } 44 | -------------------------------------------------------------------------------- /_examples/notusingutil/PickFolder.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/harry1453/go-common-file-dialog/cfd" 5 | "log" 6 | ) 7 | 8 | func main() { 9 | pickFolderDialog, err := cfd.NewSelectFolderDialog(cfd.DialogConfig{ 10 | Title: "Pick Folder", 11 | Role: "PickFolderExample", 12 | }) 13 | if err != nil { 14 | log.Fatal(err) 15 | } 16 | if err := pickFolderDialog.Show(); err != nil { 17 | log.Fatal(err) 18 | } 19 | result, err := pickFolderDialog.GetResult() 20 | if err == cfd.ErrorCancelled { 21 | log.Fatal("Dialog was cancelled by the user.") 22 | } else if err != nil { 23 | log.Fatal(err) 24 | } 25 | log.Printf("Chosen folder: %s\n", result) 26 | } 27 | -------------------------------------------------------------------------------- /_examples/notusingutil/SaveFile.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/harry1453/go-common-file-dialog/cfd" 5 | "log" 6 | ) 7 | 8 | func main() { 9 | saveDialog, err := cfd.NewSaveFileDialog(cfd.DialogConfig{ 10 | Title: "Save A File", 11 | Role: "SaveFileExample", 12 | FileFilters: []cfd.FileFilter{ 13 | { 14 | DisplayName: "Text Files (*.txt)", 15 | Pattern: "*.txt", 16 | }, 17 | { 18 | DisplayName: "Image Files (*.jpg, *.png)", 19 | Pattern: "*.jpg;*.png", 20 | }, 21 | { 22 | DisplayName: "All Files (*.*)", 23 | Pattern: "*.*", 24 | }, 25 | }, 26 | SelectedFileFilterIndex: 1, 27 | FileName: "image.jpg", 28 | DefaultExtension: "jpg", 29 | }) 30 | if err != nil { 31 | log.Fatal(err) 32 | } 33 | if err := saveDialog.Show(); err != nil { 34 | log.Fatal(err) 35 | } 36 | result, err := saveDialog.GetResult() 37 | if err == cfd.ErrorCancelled { 38 | log.Fatal("Dialog was cancelled by the user.") 39 | } else if err != nil { 40 | log.Fatal(err) 41 | } 42 | log.Printf("Chosen file: %s\n", result) 43 | } 44 | -------------------------------------------------------------------------------- /_examples/usingutil/OpenFile.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/harry1453/go-common-file-dialog/cfd" 5 | "github.com/harry1453/go-common-file-dialog/cfdutil" 6 | "log" 7 | ) 8 | 9 | func main() { 10 | result, err := cfdutil.ShowOpenFileDialog(cfd.DialogConfig{ 11 | Title: "Open A File", 12 | Role: "OpenFileExample", 13 | FileFilters: []cfd.FileFilter{ 14 | { 15 | DisplayName: "Text Files (*.txt)", 16 | Pattern: "*.txt", 17 | }, 18 | { 19 | DisplayName: "Image Files (*.jpg, *.png)", 20 | Pattern: "*.jpg;*.png", 21 | }, 22 | { 23 | DisplayName: "All Files (*.*)", 24 | Pattern: "*.*", 25 | }, 26 | }, 27 | SelectedFileFilterIndex: 2, 28 | FileName: "file.txt", 29 | DefaultExtension: "txt", 30 | }) 31 | if err == cfd.ErrorCancelled { 32 | log.Fatal("Dialog was cancelled by the user.") 33 | } else if err != nil { 34 | log.Fatal(err) 35 | } 36 | log.Printf("Chosen file: %s\n", result) 37 | } 38 | -------------------------------------------------------------------------------- /_examples/usingutil/OpenMultipleFiles.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/harry1453/go-common-file-dialog/cfd" 5 | "github.com/harry1453/go-common-file-dialog/cfdutil" 6 | "log" 7 | ) 8 | 9 | func main() { 10 | results, err := cfdutil.ShowOpenMultipleFilesDialog(cfd.DialogConfig{ 11 | Title: "Open Multiple Files", 12 | Role: "OpenFilesExample", 13 | FileFilters: []cfd.FileFilter{ 14 | { 15 | DisplayName: "Text Files (*.txt)", 16 | Pattern: "*.txt", 17 | }, 18 | { 19 | DisplayName: "Image Files (*.jpg, *.png)", 20 | Pattern: "*.jpg;*.png", 21 | }, 22 | { 23 | DisplayName: "All Files (*.*)", 24 | Pattern: "*.*", 25 | }, 26 | }, 27 | SelectedFileFilterIndex: 2, 28 | FileName: "file.txt", 29 | DefaultExtension: "txt", 30 | }) 31 | if err == cfd.ErrorCancelled { 32 | log.Fatal("Dialog was cancelled by the user.") 33 | } else if err != nil { 34 | log.Fatal(err) 35 | } 36 | log.Printf("Chosen file(s): %s\n", results) 37 | } 38 | -------------------------------------------------------------------------------- /_examples/usingutil/PickFolder.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/harry1453/go-common-file-dialog/cfd" 5 | "github.com/harry1453/go-common-file-dialog/cfdutil" 6 | "log" 7 | ) 8 | 9 | func main() { 10 | result, err := cfdutil.ShowPickFolderDialog(cfd.DialogConfig{ 11 | Title: "Pick Folder", 12 | Role: "PickFolderExample", 13 | Folder: "C:\\", 14 | }) 15 | if err == cfd.ErrorCancelled { 16 | log.Fatal("Dialog was cancelled by the user.") 17 | } else if err != nil { 18 | log.Fatal(err) 19 | } 20 | log.Printf("Chosen folder: %s\n", result) 21 | } 22 | -------------------------------------------------------------------------------- /_examples/usingutil/SaveFile.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/harry1453/go-common-file-dialog/cfd" 5 | "github.com/harry1453/go-common-file-dialog/cfdutil" 6 | "log" 7 | ) 8 | 9 | func main() { 10 | result, err := cfdutil.ShowSaveFileDialog(cfd.DialogConfig{ 11 | Title: "Save A File", 12 | Role: "SaveFileExample", 13 | FileFilters: []cfd.FileFilter{ 14 | { 15 | DisplayName: "Text Files (*.txt)", 16 | Pattern: "*.txt", 17 | }, 18 | { 19 | DisplayName: "Image Files (*.jpg, *.png)", 20 | Pattern: "*.jpg;*.png", 21 | }, 22 | { 23 | DisplayName: "All Files (*.*)", 24 | Pattern: "*.*", 25 | }, 26 | }, 27 | SelectedFileFilterIndex: 1, 28 | FileName: "image.jpg", 29 | DefaultExtension: "jpg", 30 | }) 31 | if err == cfd.ErrorCancelled { 32 | log.Fatal("Dialog was cancelled by the user.") 33 | } else if err != nil { 34 | log.Fatal(err) 35 | } 36 | log.Printf("Chosen file: %s\n", result) 37 | } 38 | -------------------------------------------------------------------------------- /cfd/CommonFileDialog.go: -------------------------------------------------------------------------------- 1 | // Cross-platform. 2 | 3 | // Common File Dialogs 4 | package cfd 5 | 6 | type Dialog interface { 7 | // Show the dialog to the user. 8 | // Blocks until the user has closed the dialog. 9 | Show() error 10 | // Sets the dialog's parent window. Use 0 to set the dialog to have no parent window. 11 | SetParentWindowHandle(hwnd uintptr) 12 | // Show the dialog to the user. 13 | // Blocks until the user has closed the dialog and returns their selection. 14 | // Returns an error if the user cancelled the dialog. 15 | // Do not use for the Open Multiple Files dialog. Use ShowAndGetResults instead. 16 | ShowAndGetResult() (string, error) 17 | // Sets the title of the dialog window. 18 | SetTitle(title string) error 19 | // Sets the "role" of the dialog. This is used to derive the dialog's GUID, which the 20 | // OS will use to differentiate it from dialogs that are intended for other purposes. 21 | // This means that, for example, a dialog with role "Import" will have a different 22 | // previous location that it will open to than a dialog with role "Open". Can be any string. 23 | SetRole(role string) error 24 | // Sets the folder used as a default if there is not a recently used folder value available 25 | SetDefaultFolder(defaultFolder string) error 26 | // Sets the folder that the dialog always opens to. 27 | // If this is set, it will override the "default folder" behaviour and the dialog will always open to this folder. 28 | SetFolder(folder string) error 29 | // Gets the selected file or folder path, as an absolute path eg. "C:\Folder\file.txt" 30 | // Do not use for the Open Multiple Files dialog. Use GetResults instead. 31 | GetResult() (string, error) 32 | // Sets the file name, I.E. the contents of the file name text box. 33 | // For Select Folder Dialog, sets folder name. 34 | SetFileName(fileName string) error 35 | // Release the resources allocated to this Dialog. 36 | // Should be called when the dialog is finished with. 37 | Release() error 38 | } 39 | 40 | type FileDialog interface { 41 | Dialog 42 | // Set the list of file filters that the user can select. 43 | SetFileFilters(fileFilter []FileFilter) error 44 | // Set the selected item from the list of file filters (set using SetFileFilters) by its index. Defaults to 0 (the first item in the list) if not called. 45 | SetSelectedFileFilterIndex(index uint) error 46 | // Sets the default extension applied when a user does not provide one as part of the file name. 47 | // If the user selects a different file filter, the default extension will be automatically updated to match the new file filter. 48 | // For Open / Open Multiple File Dialog, this only has an effect when the user specifies a file name with no extension and a file with the default extension exists. 49 | // For Save File Dialog, this extension will be used whenever a user does not specify an extension. 50 | SetDefaultExtension(defaultExtension string) error 51 | } 52 | 53 | type OpenFileDialog interface { 54 | FileDialog 55 | } 56 | 57 | type OpenMultipleFilesDialog interface { 58 | FileDialog 59 | // Show the dialog to the user. 60 | // Blocks until the user has closed the dialog and returns the selected files. 61 | ShowAndGetResults() ([]string, error) 62 | // Gets the selected file paths, as absolute paths eg. "C:\Folder\file.txt" 63 | GetResults() ([]string, error) 64 | } 65 | 66 | type SelectFolderDialog interface { 67 | Dialog 68 | } 69 | 70 | type SaveFileDialog interface { // TODO Properties 71 | FileDialog 72 | } 73 | -------------------------------------------------------------------------------- /cfd/CommonFileDialog_nonWindows.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | 3 | package cfd 4 | 5 | import "fmt" 6 | 7 | var unsupportedError = fmt.Errorf("common file dialogs are only available on windows") 8 | 9 | // TODO doc 10 | func NewOpenFileDialog(config DialogConfig) (OpenFileDialog, error) { 11 | return nil, unsupportedError 12 | } 13 | 14 | // TODO doc 15 | func NewOpenMultipleFilesDialog(config DialogConfig) (OpenMultipleFilesDialog, error) { 16 | return nil, unsupportedError 17 | } 18 | 19 | // TODO doc 20 | func NewSelectFolderDialog(config DialogConfig) (SelectFolderDialog, error) { 21 | return nil, unsupportedError 22 | } 23 | 24 | // TODO doc 25 | func NewSaveFileDialog(config DialogConfig) (SaveFileDialog, error) { 26 | return nil, unsupportedError 27 | } 28 | -------------------------------------------------------------------------------- /cfd/CommonFileDialog_windows.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | package cfd 4 | 5 | import "github.com/go-ole/go-ole" 6 | 7 | func initialize() { 8 | // Swallow error 9 | _ = ole.CoInitializeEx(0, ole.COINIT_APARTMENTTHREADED|ole.COINIT_DISABLE_OLE1DDE) 10 | } 11 | 12 | // TODO doc 13 | func NewOpenFileDialog(config DialogConfig) (OpenFileDialog, error) { 14 | initialize() 15 | 16 | openDialog, err := newIFileOpenDialog() 17 | if err != nil { 18 | return nil, err 19 | } 20 | err = config.apply(openDialog) 21 | if err != nil { 22 | return nil, err 23 | } 24 | return openDialog, nil 25 | } 26 | 27 | // TODO doc 28 | func NewOpenMultipleFilesDialog(config DialogConfig) (OpenMultipleFilesDialog, error) { 29 | initialize() 30 | 31 | openDialog, err := newIFileOpenDialog() 32 | if err != nil { 33 | return nil, err 34 | } 35 | err = config.apply(openDialog) 36 | if err != nil { 37 | return nil, err 38 | } 39 | err = openDialog.setIsMultiselect(true) 40 | if err != nil { 41 | return nil, err 42 | } 43 | return openDialog, nil 44 | } 45 | 46 | // TODO doc 47 | func NewSelectFolderDialog(config DialogConfig) (SelectFolderDialog, error) { 48 | initialize() 49 | 50 | openDialog, err := newIFileOpenDialog() 51 | if err != nil { 52 | return nil, err 53 | } 54 | err = config.apply(openDialog) 55 | if err != nil { 56 | return nil, err 57 | } 58 | err = openDialog.setPickFolders(true) 59 | if err != nil { 60 | return nil, err 61 | } 62 | return openDialog, nil 63 | } 64 | 65 | // TODO doc 66 | func NewSaveFileDialog(config DialogConfig) (SaveFileDialog, error) { 67 | initialize() 68 | 69 | saveDialog, err := newIFileSaveDialog() 70 | if err != nil { 71 | return nil, err 72 | } 73 | err = config.apply(saveDialog) 74 | if err != nil { 75 | return nil, err 76 | } 77 | return saveDialog, nil 78 | } 79 | -------------------------------------------------------------------------------- /cfd/DialogConfig.go: -------------------------------------------------------------------------------- 1 | // Cross-platform. 2 | 3 | package cfd 4 | 5 | import ( 6 | "errors" 7 | "os" 8 | ) 9 | 10 | type FileFilter struct { 11 | // The display name of the filter (That is shown to the user) 12 | DisplayName string 13 | // The filter pattern. Eg. "*.txt;*.png" to select all txt and png files, "*.*" to select any files, etc. 14 | Pattern string 15 | } 16 | 17 | type DialogConfig struct { 18 | // The title of the dialog 19 | Title string 20 | // The role of the dialog. This is used to derive the dialog's GUID, which the 21 | // OS will use to differentiate it from dialogs that are intended for other purposes. 22 | // This means that, for example, a dialog with role "Import" will have a different 23 | // previous location that it will open to than a dialog with role "Open". Can be any string. 24 | Role string 25 | // The default folder - the folder that is used the first time the user opens it 26 | // (after the first time their last used location is used). 27 | DefaultFolder string 28 | // The initial folder - the folder that the dialog always opens to if not empty. 29 | // If this is not empty, it will override the "default folder" behaviour and 30 | // the dialog will always open to this folder. 31 | Folder string 32 | // The file filters that restrict which types of files the dialog is able to choose. 33 | // Ignored by Select Folder Dialog. 34 | FileFilters []FileFilter 35 | // Sets the initially selected file filter. This is an index of FileFilters. 36 | // Ignored by Select Folder Dialog. 37 | SelectedFileFilterIndex uint 38 | // The initial name of the file (I.E. the text in the file name text box) when the user opens the dialog. 39 | // For the Select Folder Dialog, this sets the initial folder name. 40 | FileName string 41 | // The default extension applied when a user does not provide one as part of the file name. 42 | // If the user selects a different file filter, the default extension will be automatically updated to match the new file filter. 43 | // For Open / Open Multiple File Dialog, this only has an effect when the user specifies a file name with no extension and a file with the default extension exists. 44 | // For Save File Dialog, this extension will be used whenever a user does not specify an extension. 45 | // Ignored by Select Folder Dialog. 46 | DefaultExtension string 47 | // ParentWindowHandle is the handle (HWND) to the parent window of the dialog. 48 | // If left as 0 / nil, the dialog will have no parent window. 49 | ParentWindowHandle uintptr 50 | } 51 | 52 | var defaultFilters = []FileFilter{ 53 | { 54 | DisplayName: "All Files (*.*)", 55 | Pattern: "*.*", 56 | }, 57 | } 58 | 59 | func (config *DialogConfig) apply(dialog Dialog) (err error) { 60 | if config.Title != "" { 61 | err = dialog.SetTitle(config.Title) 62 | if err != nil { 63 | return 64 | } 65 | } 66 | 67 | if config.Role != "" { 68 | err = dialog.SetRole(config.Role) 69 | if err != nil { 70 | return 71 | } 72 | } 73 | 74 | if config.Folder != "" { 75 | _, err = os.Stat(config.Folder) 76 | if err != nil { 77 | return 78 | } 79 | err = dialog.SetFolder(config.Folder) 80 | if err != nil { 81 | return 82 | } 83 | } 84 | 85 | if config.DefaultFolder != "" { 86 | _, err = os.Stat(config.DefaultFolder) 87 | if err != nil { 88 | return 89 | } 90 | err = dialog.SetDefaultFolder(config.DefaultFolder) 91 | if err != nil { 92 | return 93 | } 94 | } 95 | 96 | if config.FileName != "" { 97 | err = dialog.SetFileName(config.FileName) 98 | if err != nil { 99 | return 100 | } 101 | } 102 | 103 | dialog.SetParentWindowHandle(config.ParentWindowHandle) 104 | 105 | if dialog, ok := dialog.(FileDialog); ok { 106 | var fileFilters []FileFilter 107 | if config.FileFilters != nil && len(config.FileFilters) > 0 { 108 | fileFilters = config.FileFilters 109 | } else { 110 | fileFilters = defaultFilters 111 | } 112 | err = dialog.SetFileFilters(fileFilters) 113 | if err != nil { 114 | return 115 | } 116 | 117 | if config.SelectedFileFilterIndex != 0 { 118 | if config.SelectedFileFilterIndex > uint(len(fileFilters)) { 119 | err = errors.New("selected file filter index out of range") 120 | return 121 | } 122 | err = dialog.SetSelectedFileFilterIndex(config.SelectedFileFilterIndex) 123 | if err != nil { 124 | return 125 | } 126 | } 127 | 128 | if config.DefaultExtension != "" { 129 | err = dialog.SetDefaultExtension(config.DefaultExtension) 130 | if err != nil { 131 | return 132 | } 133 | } 134 | } 135 | 136 | return 137 | } 138 | -------------------------------------------------------------------------------- /cfd/errors.go: -------------------------------------------------------------------------------- 1 | package cfd 2 | 3 | import "errors" 4 | 5 | var ( 6 | ErrorCancelled = errors.New("cancelled by user") 7 | ) 8 | -------------------------------------------------------------------------------- /cfd/iFileOpenDialog.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package cfd 5 | 6 | import ( 7 | "syscall" 8 | "unsafe" 9 | 10 | "github.com/go-ole/go-ole" 11 | "github.com/harry1453/go-common-file-dialog/util" 12 | ) 13 | 14 | var ( 15 | fileOpenDialogCLSID = ole.NewGUID("{DC1C5A9C-E88A-4dde-A5A1-60F82A20AEF7}") 16 | fileOpenDialogIID = ole.NewGUID("{d57c7288-d4ad-4768-be02-9d969532d960}") 17 | ) 18 | 19 | type iFileOpenDialog struct { 20 | vtbl *iFileOpenDialogVtbl 21 | parentWindowHandle uintptr 22 | } 23 | 24 | type iFileOpenDialogVtbl struct { 25 | iFileDialogVtbl 26 | 27 | GetResults uintptr // func (ppenum **IShellItemArray) HRESULT 28 | GetSelectedItems uintptr 29 | } 30 | 31 | func newIFileOpenDialog() (*iFileOpenDialog, error) { 32 | if unknown, err := ole.CreateInstance(fileOpenDialogCLSID, fileOpenDialogIID); err == nil { 33 | return (*iFileOpenDialog)(unsafe.Pointer(unknown)), nil 34 | } else { 35 | return nil, err 36 | } 37 | } 38 | 39 | func (fileOpenDialog *iFileOpenDialog) Show() error { 40 | return fileOpenDialog.vtbl.show(unsafe.Pointer(fileOpenDialog), fileOpenDialog.parentWindowHandle) 41 | } 42 | 43 | func (fileOpenDialog *iFileOpenDialog) SetParentWindowHandle(hwnd uintptr) { 44 | fileOpenDialog.parentWindowHandle = hwnd 45 | } 46 | 47 | func (fileOpenDialog *iFileOpenDialog) ShowAndGetResult() (string, error) { 48 | isMultiselect, err := fileOpenDialog.isMultiselect() 49 | if err != nil { 50 | return "", err 51 | } 52 | if isMultiselect { 53 | // We should panic as this error is caused by the developer using the library 54 | panic("use ShowAndGetResults for open multiple files dialog") 55 | } 56 | if err := fileOpenDialog.Show(); err != nil { 57 | return "", err 58 | } 59 | return fileOpenDialog.GetResult() 60 | } 61 | 62 | func (fileOpenDialog *iFileOpenDialog) ShowAndGetResults() ([]string, error) { 63 | isMultiselect, err := fileOpenDialog.isMultiselect() 64 | if err != nil { 65 | return nil, err 66 | } 67 | if !isMultiselect { 68 | // We should panic as this error is caused by the developer using the library 69 | panic("use ShowAndGetResult for open single file dialog") 70 | } 71 | if err := fileOpenDialog.Show(); err != nil { 72 | return nil, err 73 | } 74 | return fileOpenDialog.GetResults() 75 | } 76 | 77 | func (fileOpenDialog *iFileOpenDialog) SetTitle(title string) error { 78 | return fileOpenDialog.vtbl.setTitle(unsafe.Pointer(fileOpenDialog), title) 79 | } 80 | 81 | func (fileOpenDialog *iFileOpenDialog) GetResult() (string, error) { 82 | isMultiselect, err := fileOpenDialog.isMultiselect() 83 | if err != nil { 84 | return "", err 85 | } 86 | if isMultiselect { 87 | // We should panic as this error is caused by the developer using the library 88 | panic("use GetResults for open multiple files dialog") 89 | } 90 | return fileOpenDialog.vtbl.getResultString(unsafe.Pointer(fileOpenDialog)) 91 | } 92 | 93 | func (fileOpenDialog *iFileOpenDialog) Release() error { 94 | return fileOpenDialog.vtbl.release(unsafe.Pointer(fileOpenDialog)) 95 | } 96 | 97 | func (fileOpenDialog *iFileOpenDialog) SetDefaultFolder(defaultFolderPath string) error { 98 | return fileOpenDialog.vtbl.setDefaultFolder(unsafe.Pointer(fileOpenDialog), defaultFolderPath) 99 | } 100 | 101 | func (fileOpenDialog *iFileOpenDialog) SetFolder(defaultFolderPath string) error { 102 | return fileOpenDialog.vtbl.setFolder(unsafe.Pointer(fileOpenDialog), defaultFolderPath) 103 | } 104 | 105 | func (fileOpenDialog *iFileOpenDialog) SetFileFilters(filter []FileFilter) error { 106 | return fileOpenDialog.vtbl.setFileTypes(unsafe.Pointer(fileOpenDialog), filter) 107 | } 108 | 109 | func (fileOpenDialog *iFileOpenDialog) SetRole(role string) error { 110 | return fileOpenDialog.vtbl.setClientGuid(unsafe.Pointer(fileOpenDialog), util.StringToUUID(role)) 111 | } 112 | 113 | // This should only be callable when the user asks for a multi select because 114 | // otherwise they will be given the Dialog interface which does not expose this function. 115 | func (fileOpenDialog *iFileOpenDialog) GetResults() ([]string, error) { 116 | isMultiselect, err := fileOpenDialog.isMultiselect() 117 | if err != nil { 118 | return nil, err 119 | } 120 | if !isMultiselect { 121 | // We should panic as this error is caused by the developer using the library 122 | panic("use GetResult for open single file dialog") 123 | } 124 | return fileOpenDialog.vtbl.getResultsStrings(unsafe.Pointer(fileOpenDialog)) 125 | } 126 | 127 | func (fileOpenDialog *iFileOpenDialog) SetDefaultExtension(defaultExtension string) error { 128 | return fileOpenDialog.vtbl.setDefaultExtension(unsafe.Pointer(fileOpenDialog), defaultExtension) 129 | } 130 | 131 | func (fileOpenDialog *iFileOpenDialog) SetFileName(initialFileName string) error { 132 | return fileOpenDialog.vtbl.setFileName(unsafe.Pointer(fileOpenDialog), initialFileName) 133 | } 134 | 135 | func (fileOpenDialog *iFileOpenDialog) SetSelectedFileFilterIndex(index uint) error { 136 | return fileOpenDialog.vtbl.setSelectedFileFilterIndex(unsafe.Pointer(fileOpenDialog), index) 137 | } 138 | 139 | func (fileOpenDialog *iFileOpenDialog) setPickFolders(pickFolders bool) error { 140 | const FosPickfolders = 0x20 141 | if pickFolders { 142 | return fileOpenDialog.vtbl.addOption(unsafe.Pointer(fileOpenDialog), FosPickfolders) 143 | } else { 144 | return fileOpenDialog.vtbl.removeOption(unsafe.Pointer(fileOpenDialog), FosPickfolders) 145 | } 146 | } 147 | 148 | const FosAllowMultiselect = 0x200 149 | 150 | func (fileOpenDialog *iFileOpenDialog) isMultiselect() (bool, error) { 151 | options, err := fileOpenDialog.vtbl.getOptions(unsafe.Pointer(fileOpenDialog)) 152 | if err != nil { 153 | return false, err 154 | } 155 | return options&FosAllowMultiselect != 0, nil 156 | } 157 | 158 | func (fileOpenDialog *iFileOpenDialog) setIsMultiselect(isMultiselect bool) error { 159 | if isMultiselect { 160 | return fileOpenDialog.vtbl.addOption(unsafe.Pointer(fileOpenDialog), FosAllowMultiselect) 161 | } else { 162 | return fileOpenDialog.vtbl.removeOption(unsafe.Pointer(fileOpenDialog), FosAllowMultiselect) 163 | } 164 | } 165 | 166 | func (vtbl *iFileOpenDialogVtbl) getResults(objPtr unsafe.Pointer) (*iShellItemArray, error) { 167 | var shellItemArray *iShellItemArray 168 | ret, _, _ := syscall.SyscallN(vtbl.GetResults, 169 | uintptr(objPtr), 170 | uintptr(unsafe.Pointer(&shellItemArray))) 171 | return shellItemArray, hresultToError(ret) 172 | } 173 | 174 | func (vtbl *iFileOpenDialogVtbl) getResultsStrings(objPtr unsafe.Pointer) ([]string, error) { 175 | shellItemArray, err := vtbl.getResults(objPtr) 176 | if err != nil { 177 | return nil, err 178 | } 179 | if shellItemArray == nil { 180 | return nil, ErrorCancelled 181 | } 182 | defer shellItemArray.vtbl.release(unsafe.Pointer(shellItemArray)) 183 | count, err := shellItemArray.vtbl.getCount(unsafe.Pointer(shellItemArray)) 184 | if err != nil { 185 | return nil, err 186 | } 187 | var results []string 188 | for i := uintptr(0); i < count; i++ { 189 | newItem, err := shellItemArray.vtbl.getItemAt(unsafe.Pointer(shellItemArray), i) 190 | if err != nil { 191 | return nil, err 192 | } 193 | results = append(results, newItem) 194 | } 195 | return results, nil 196 | } 197 | -------------------------------------------------------------------------------- /cfd/iFileSaveDialog.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | package cfd 4 | 5 | import ( 6 | "github.com/go-ole/go-ole" 7 | "github.com/harry1453/go-common-file-dialog/util" 8 | "unsafe" 9 | ) 10 | 11 | var ( 12 | saveFileDialogCLSID = ole.NewGUID("{C0B4E2F3-BA21-4773-8DBA-335EC946EB8B}") 13 | saveFileDialogIID = ole.NewGUID("{84bccd23-5fde-4cdb-aea4-af64b83d78ab}") 14 | ) 15 | 16 | type iFileSaveDialog struct { 17 | vtbl *iFileSaveDialogVtbl 18 | parentWindowHandle uintptr 19 | } 20 | 21 | type iFileSaveDialogVtbl struct { 22 | iFileDialogVtbl 23 | 24 | SetSaveAsItem uintptr 25 | SetProperties uintptr 26 | SetCollectedProperties uintptr 27 | GetProperties uintptr 28 | ApplyProperties uintptr 29 | } 30 | 31 | func newIFileSaveDialog() (*iFileSaveDialog, error) { 32 | if unknown, err := ole.CreateInstance(saveFileDialogCLSID, saveFileDialogIID); err == nil { 33 | return (*iFileSaveDialog)(unsafe.Pointer(unknown)), nil 34 | } else { 35 | return nil, err 36 | } 37 | } 38 | 39 | func (fileSaveDialog *iFileSaveDialog) Show() error { 40 | return fileSaveDialog.vtbl.show(unsafe.Pointer(fileSaveDialog), fileSaveDialog.parentWindowHandle) 41 | } 42 | 43 | func (fileSaveDialog *iFileSaveDialog) SetParentWindowHandle(hwnd uintptr) { 44 | fileSaveDialog.parentWindowHandle = hwnd 45 | } 46 | 47 | func (fileSaveDialog *iFileSaveDialog) ShowAndGetResult() (string, error) { 48 | if err := fileSaveDialog.Show(); err != nil { 49 | return "", err 50 | } 51 | return fileSaveDialog.GetResult() 52 | } 53 | 54 | func (fileSaveDialog *iFileSaveDialog) SetTitle(title string) error { 55 | return fileSaveDialog.vtbl.setTitle(unsafe.Pointer(fileSaveDialog), title) 56 | } 57 | 58 | func (fileSaveDialog *iFileSaveDialog) GetResult() (string, error) { 59 | return fileSaveDialog.vtbl.getResultString(unsafe.Pointer(fileSaveDialog)) 60 | } 61 | 62 | func (fileSaveDialog *iFileSaveDialog) Release() error { 63 | return fileSaveDialog.vtbl.release(unsafe.Pointer(fileSaveDialog)) 64 | } 65 | 66 | func (fileSaveDialog *iFileSaveDialog) SetDefaultFolder(defaultFolderPath string) error { 67 | return fileSaveDialog.vtbl.setDefaultFolder(unsafe.Pointer(fileSaveDialog), defaultFolderPath) 68 | } 69 | 70 | func (fileSaveDialog *iFileSaveDialog) SetFolder(defaultFolderPath string) error { 71 | return fileSaveDialog.vtbl.setFolder(unsafe.Pointer(fileSaveDialog), defaultFolderPath) 72 | } 73 | 74 | func (fileSaveDialog *iFileSaveDialog) SetFileFilters(filter []FileFilter) error { 75 | return fileSaveDialog.vtbl.setFileTypes(unsafe.Pointer(fileSaveDialog), filter) 76 | } 77 | 78 | func (fileSaveDialog *iFileSaveDialog) SetRole(role string) error { 79 | return fileSaveDialog.vtbl.setClientGuid(unsafe.Pointer(fileSaveDialog), util.StringToUUID(role)) 80 | } 81 | 82 | func (fileSaveDialog *iFileSaveDialog) SetDefaultExtension(defaultExtension string) error { 83 | return fileSaveDialog.vtbl.setDefaultExtension(unsafe.Pointer(fileSaveDialog), defaultExtension) 84 | } 85 | 86 | func (fileSaveDialog *iFileSaveDialog) SetFileName(initialFileName string) error { 87 | return fileSaveDialog.vtbl.setFileName(unsafe.Pointer(fileSaveDialog), initialFileName) 88 | } 89 | 90 | func (fileSaveDialog *iFileSaveDialog) SetSelectedFileFilterIndex(index uint) error { 91 | return fileSaveDialog.vtbl.setSelectedFileFilterIndex(unsafe.Pointer(fileSaveDialog), index) 92 | } 93 | -------------------------------------------------------------------------------- /cfd/iShellItem.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package cfd 5 | 6 | import ( 7 | "syscall" 8 | "unsafe" 9 | 10 | "github.com/go-ole/go-ole" 11 | ) 12 | 13 | var ( 14 | procSHCreateItemFromParsingName = syscall.NewLazyDLL("Shell32.dll").NewProc("SHCreateItemFromParsingName") 15 | iidShellItem = ole.NewGUID("43826d1e-e718-42ee-bc55-a1e261c37bfe") 16 | ) 17 | 18 | type iShellItem struct { 19 | vtbl *iShellItemVtbl 20 | } 21 | 22 | type iShellItemVtbl struct { 23 | iUnknownVtbl 24 | BindToHandler uintptr 25 | GetParent uintptr 26 | GetDisplayName uintptr // func (sigdnName SIGDN, ppszName *LPWSTR) HRESULT 27 | GetAttributes uintptr 28 | Compare uintptr 29 | } 30 | 31 | func newIShellItem(path string) (*iShellItem, error) { 32 | var shellItem *iShellItem 33 | pathPtr := ole.SysAllocString(path) 34 | defer ole.SysFreeString(pathPtr) 35 | ret, _, _ := procSHCreateItemFromParsingName.Call( 36 | uintptr(unsafe.Pointer(pathPtr)), 37 | 0, 38 | uintptr(unsafe.Pointer(iidShellItem)), 39 | uintptr(unsafe.Pointer(&shellItem))) 40 | return shellItem, hresultToError(ret) 41 | } 42 | 43 | func (vtbl *iShellItemVtbl) getDisplayName(objPtr unsafe.Pointer) (string, error) { 44 | var ptr *uint16 45 | ret, _, _ := syscall.SyscallN(vtbl.GetDisplayName, 46 | uintptr(objPtr), 47 | 0x80058000, // SIGDN_FILESYSPATH 48 | uintptr(unsafe.Pointer(&ptr))) 49 | if err := hresultToError(ret); err != nil { 50 | return "", err 51 | } 52 | defer ole.CoTaskMemFree(uintptr(unsafe.Pointer(ptr))) 53 | return ole.LpOleStrToString(ptr), nil 54 | } 55 | -------------------------------------------------------------------------------- /cfd/iShellItemArray.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package cfd 5 | 6 | import ( 7 | "syscall" 8 | "unsafe" 9 | 10 | "github.com/go-ole/go-ole" 11 | ) 12 | 13 | const ( 14 | iidShellItemArrayGUID = "{b63ea76d-1f85-456f-a19c-48159efa858b}" 15 | ) 16 | 17 | var ( 18 | iidShellItemArray *ole.GUID 19 | ) 20 | 21 | func init() { 22 | iidShellItemArray, _ = ole.IIDFromString(iidShellItemArrayGUID) 23 | } 24 | 25 | type iShellItemArray struct { 26 | vtbl *iShellItemArrayVtbl 27 | } 28 | 29 | type iShellItemArrayVtbl struct { 30 | iUnknownVtbl 31 | BindToHandler uintptr 32 | GetPropertyStore uintptr 33 | GetPropertyDescriptionList uintptr 34 | GetAttributes uintptr 35 | GetCount uintptr // func (pdwNumItems *DWORD) HRESULT 36 | GetItemAt uintptr // func (dwIndex DWORD, ppsi **IShellItem) HRESULT 37 | EnumItems uintptr 38 | } 39 | 40 | func (vtbl *iShellItemArrayVtbl) getCount(objPtr unsafe.Pointer) (uintptr, error) { 41 | var count uintptr 42 | ret, _, _ := syscall.SyscallN(vtbl.GetCount, 43 | uintptr(objPtr), 44 | uintptr(unsafe.Pointer(&count))) 45 | if err := hresultToError(ret); err != nil { 46 | return 0, err 47 | } 48 | return count, nil 49 | } 50 | 51 | func (vtbl *iShellItemArrayVtbl) getItemAt(objPtr unsafe.Pointer, index uintptr) (string, error) { 52 | var shellItem *iShellItem 53 | ret, _, _ := syscall.SyscallN(vtbl.GetItemAt, 54 | uintptr(objPtr), 55 | index, 56 | uintptr(unsafe.Pointer(&shellItem))) 57 | if err := hresultToError(ret); err != nil { 58 | return "", err 59 | } 60 | if shellItem == nil { 61 | return "", ErrorCancelled 62 | } 63 | defer shellItem.vtbl.release(unsafe.Pointer(shellItem)) 64 | return shellItem.vtbl.getDisplayName(unsafe.Pointer(shellItem)) 65 | } 66 | -------------------------------------------------------------------------------- /cfd/vtblCommon.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | package cfd 4 | 5 | type comDlgFilterSpec struct { 6 | pszName *int16 7 | pszSpec *int16 8 | } 9 | 10 | type iUnknownVtbl struct { 11 | QueryInterface uintptr 12 | AddRef uintptr 13 | Release uintptr 14 | } 15 | 16 | type iModalWindowVtbl struct { 17 | iUnknownVtbl 18 | Show uintptr // func (hwndOwner HWND) HRESULT 19 | } 20 | 21 | type iFileDialogVtbl struct { 22 | iModalWindowVtbl 23 | SetFileTypes uintptr // func (cFileTypes UINT, rgFilterSpec *COMDLG_FILTERSPEC) HRESULT 24 | SetFileTypeIndex uintptr // func(iFileType UINT) HRESULT 25 | GetFileTypeIndex uintptr 26 | Advise uintptr 27 | Unadvise uintptr 28 | SetOptions uintptr // func (fos FILEOPENDIALOGOPTIONS) HRESULT 29 | GetOptions uintptr // func (pfos *FILEOPENDIALOGOPTIONS) HRESULT 30 | SetDefaultFolder uintptr // func (psi *IShellItem) HRESULT 31 | SetFolder uintptr // func (psi *IShellItem) HRESULT 32 | GetFolder uintptr 33 | GetCurrentSelection uintptr 34 | SetFileName uintptr // func (pszName LPCWSTR) HRESULT 35 | GetFileName uintptr 36 | SetTitle uintptr // func(pszTitle LPCWSTR) HRESULT 37 | SetOkButtonLabel uintptr 38 | SetFileNameLabel uintptr 39 | GetResult uintptr // func (ppsi **IShellItem) HRESULT 40 | AddPlace uintptr 41 | SetDefaultExtension uintptr // func (pszDefaultExtension LPCWSTR) HRESULT 42 | // This can only be used from a callback. 43 | Close uintptr 44 | SetClientGuid uintptr // func (guid REFGUID) HRESULT 45 | ClearClientData uintptr 46 | SetFilter uintptr 47 | } 48 | -------------------------------------------------------------------------------- /cfd/vtblCommonFunc.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package cfd 5 | 6 | import ( 7 | "fmt" 8 | "strings" 9 | "syscall" 10 | "unsafe" 11 | 12 | "github.com/go-ole/go-ole" 13 | ) 14 | 15 | func hresultToError(hr uintptr) error { 16 | if hr < 0 { 17 | return ole.NewError(hr) 18 | } 19 | return nil 20 | } 21 | 22 | func (vtbl *iUnknownVtbl) release(objPtr unsafe.Pointer) error { 23 | ret, _, _ := syscall.SyscallN(vtbl.Release, 24 | uintptr(objPtr)) 25 | return hresultToError(ret) 26 | } 27 | 28 | func (vtbl *iModalWindowVtbl) show(objPtr unsafe.Pointer, hwnd uintptr) error { 29 | ret, _, _ := syscall.SyscallN(vtbl.Show, 30 | uintptr(objPtr), 31 | hwnd) 32 | return hresultToError(ret) 33 | } 34 | 35 | func (vtbl *iFileDialogVtbl) setFileTypes(objPtr unsafe.Pointer, filters []FileFilter) error { 36 | cFileTypes := len(filters) 37 | if cFileTypes < 0 { 38 | return fmt.Errorf("must specify at least one filter") 39 | } 40 | comDlgFilterSpecs := make([]comDlgFilterSpec, cFileTypes) 41 | for i := 0; i < cFileTypes; i++ { 42 | filter := &filters[i] 43 | comDlgFilterSpecs[i] = comDlgFilterSpec{ 44 | pszName: ole.SysAllocString(filter.DisplayName), 45 | pszSpec: ole.SysAllocString(filter.Pattern), 46 | } 47 | } 48 | 49 | // Ensure memory is freed after use 50 | defer func() { 51 | for _, spec := range comDlgFilterSpecs { 52 | ole.SysFreeString(spec.pszName) 53 | ole.SysFreeString(spec.pszSpec) 54 | } 55 | }() 56 | 57 | ret, _, _ := syscall.SyscallN(vtbl.SetFileTypes, 58 | uintptr(objPtr), 59 | uintptr(cFileTypes), 60 | uintptr(unsafe.Pointer(&comDlgFilterSpecs[0]))) 61 | return hresultToError(ret) 62 | } 63 | 64 | // Options are: 65 | // FOS_OVERWRITEPROMPT = 0x2, 66 | // FOS_STRICTFILETYPES = 0x4, 67 | // FOS_NOCHANGEDIR = 0x8, 68 | // FOS_PICKFOLDERS = 0x20, 69 | // FOS_FORCEFILESYSTEM = 0x40, 70 | // FOS_ALLNONSTORAGEITEMS = 0x80, 71 | // FOS_NOVALIDATE = 0x100, 72 | // FOS_ALLOWMULTISELECT = 0x200, 73 | // FOS_PATHMUSTEXIST = 0x800, 74 | // FOS_FILEMUSTEXIST = 0x1000, 75 | // FOS_CREATEPROMPT = 0x2000, 76 | // FOS_SHAREAWARE = 0x4000, 77 | // FOS_NOREADONLYRETURN = 0x8000, 78 | // FOS_NOTESTFILECREATE = 0x10000, 79 | // FOS_HIDEMRUPLACES = 0x20000, 80 | // FOS_HIDEPINNEDPLACES = 0x40000, 81 | // FOS_NODEREFERENCELINKS = 0x100000, 82 | // FOS_OKBUTTONNEEDSINTERACTION = 0x200000, 83 | // FOS_DONTADDTORECENT = 0x2000000, 84 | // FOS_FORCESHOWHIDDEN = 0x10000000, 85 | // FOS_DEFAULTNOMINIMODE = 0x20000000, 86 | // FOS_FORCEPREVIEWPANEON = 0x40000000, 87 | // FOS_SUPPORTSTREAMABLEITEMS = 0x80000000 88 | func (vtbl *iFileDialogVtbl) setOptions(objPtr unsafe.Pointer, options uint32) error { 89 | ret, _, _ := syscall.SyscallN(vtbl.SetOptions, 90 | uintptr(objPtr), 91 | uintptr(options)) 92 | return hresultToError(ret) 93 | } 94 | 95 | func (vtbl *iFileDialogVtbl) getOptions(objPtr unsafe.Pointer) (uint32, error) { 96 | var options uint32 97 | ret, _, _ := syscall.SyscallN(vtbl.GetOptions, 98 | uintptr(objPtr), 99 | uintptr(unsafe.Pointer(&options))) 100 | return options, hresultToError(ret) 101 | } 102 | 103 | func (vtbl *iFileDialogVtbl) addOption(objPtr unsafe.Pointer, option uint32) error { 104 | if options, err := vtbl.getOptions(objPtr); err == nil { 105 | return vtbl.setOptions(objPtr, options|option) 106 | } else { 107 | return err 108 | } 109 | } 110 | 111 | func (vtbl *iFileDialogVtbl) removeOption(objPtr unsafe.Pointer, option uint32) error { 112 | if options, err := vtbl.getOptions(objPtr); err == nil { 113 | return vtbl.setOptions(objPtr, options&^option) 114 | } else { 115 | return err 116 | } 117 | } 118 | 119 | func (vtbl *iFileDialogVtbl) setDefaultFolder(objPtr unsafe.Pointer, path string) error { 120 | shellItem, err := newIShellItem(path) 121 | if err != nil { 122 | return err 123 | } 124 | defer shellItem.vtbl.release(unsafe.Pointer(shellItem)) 125 | ret, _, _ := syscall.SyscallN(vtbl.SetDefaultFolder, 126 | uintptr(objPtr), 127 | uintptr(unsafe.Pointer(shellItem))) 128 | return hresultToError(ret) 129 | } 130 | 131 | func (vtbl *iFileDialogVtbl) setFolder(objPtr unsafe.Pointer, path string) error { 132 | shellItem, err := newIShellItem(path) 133 | if err != nil { 134 | return err 135 | } 136 | defer shellItem.vtbl.release(unsafe.Pointer(shellItem)) 137 | ret, _, _ := syscall.SyscallN(vtbl.SetFolder, 138 | uintptr(objPtr), 139 | uintptr(unsafe.Pointer(shellItem))) 140 | return hresultToError(ret) 141 | } 142 | 143 | func (vtbl *iFileDialogVtbl) setTitle(objPtr unsafe.Pointer, title string) error { 144 | titlePtr := ole.SysAllocString(title) 145 | defer ole.SysFreeString(titlePtr) 146 | ret, _, _ := syscall.SyscallN(vtbl.SetTitle, 147 | uintptr(objPtr), 148 | uintptr(unsafe.Pointer(titlePtr))) 149 | return hresultToError(ret) 150 | } 151 | 152 | func (vtbl *iFileDialogVtbl) close(objPtr unsafe.Pointer) error { 153 | ret, _, _ := syscall.SyscallN(vtbl.Close, 154 | uintptr(objPtr)) 155 | return hresultToError(ret) 156 | } 157 | 158 | func (vtbl *iFileDialogVtbl) getResult(objPtr unsafe.Pointer) (*iShellItem, error) { 159 | var shellItem *iShellItem 160 | ret, _, _ := syscall.SyscallN(vtbl.GetResult, 161 | uintptr(objPtr), 162 | uintptr(unsafe.Pointer(&shellItem))) 163 | return shellItem, hresultToError(ret) 164 | } 165 | 166 | func (vtbl *iFileDialogVtbl) getResultString(objPtr unsafe.Pointer) (string, error) { 167 | shellItem, err := vtbl.getResult(objPtr) 168 | if err != nil { 169 | return "", err 170 | } 171 | if shellItem == nil { 172 | return "", ErrorCancelled 173 | } 174 | defer shellItem.vtbl.release(unsafe.Pointer(shellItem)) 175 | return shellItem.vtbl.getDisplayName(unsafe.Pointer(shellItem)) 176 | } 177 | 178 | func (vtbl *iFileDialogVtbl) setClientGuid(objPtr unsafe.Pointer, guid *ole.GUID) error { 179 | ret, _, _ := syscall.SyscallN(vtbl.SetClientGuid, 180 | uintptr(objPtr), 181 | uintptr(unsafe.Pointer(guid))) 182 | return hresultToError(ret) 183 | } 184 | 185 | func (vtbl *iFileDialogVtbl) setDefaultExtension(objPtr unsafe.Pointer, defaultExtension string) error { 186 | if defaultExtension[0] == '.' { 187 | defaultExtension = strings.TrimPrefix(defaultExtension, ".") 188 | } 189 | defaultExtensionPtr := ole.SysAllocString(defaultExtension) 190 | defer ole.SysFreeString(defaultExtensionPtr) 191 | ret, _, _ := syscall.SyscallN(vtbl.SetDefaultExtension, 192 | uintptr(objPtr), 193 | uintptr(unsafe.Pointer(defaultExtensionPtr))) 194 | return hresultToError(ret) 195 | } 196 | 197 | func (vtbl *iFileDialogVtbl) setFileName(objPtr unsafe.Pointer, fileName string) error { 198 | fileNamePtr := ole.SysAllocString(fileName) 199 | defer ole.SysFreeString(fileNamePtr) 200 | ret, _, _ := syscall.SyscallN(vtbl.SetFileName, 201 | uintptr(objPtr), 202 | uintptr(unsafe.Pointer(fileNamePtr))) 203 | return hresultToError(ret) 204 | } 205 | 206 | func (vtbl *iFileDialogVtbl) setSelectedFileFilterIndex(objPtr unsafe.Pointer, index uint) error { 207 | ret, _, _ := syscall.SyscallN(vtbl.SetFileTypeIndex, 208 | uintptr(objPtr), 209 | uintptr(index+1), // SetFileTypeIndex counts from 1 210 | ) 211 | return hresultToError(ret) 212 | } 213 | -------------------------------------------------------------------------------- /cfdutil/CFDUtil.go: -------------------------------------------------------------------------------- 1 | package cfdutil 2 | 3 | import ( 4 | "github.com/harry1453/go-common-file-dialog/cfd" 5 | ) 6 | 7 | // TODO doc 8 | func ShowOpenFileDialog(config cfd.DialogConfig) (string, error) { 9 | dialog, err := cfd.NewOpenFileDialog(config) 10 | if err != nil { 11 | return "", err 12 | } 13 | defer dialog.Release() 14 | return dialog.ShowAndGetResult() 15 | } 16 | 17 | // TODO doc 18 | func ShowOpenMultipleFilesDialog(config cfd.DialogConfig) ([]string, error) { 19 | dialog, err := cfd.NewOpenMultipleFilesDialog(config) 20 | if err != nil { 21 | return nil, err 22 | } 23 | defer dialog.Release() 24 | return dialog.ShowAndGetResults() 25 | } 26 | 27 | // TODO doc 28 | func ShowPickFolderDialog(config cfd.DialogConfig) (string, error) { 29 | dialog, err := cfd.NewSelectFolderDialog(config) 30 | if err != nil { 31 | return "", err 32 | } 33 | defer dialog.Release() 34 | return dialog.ShowAndGetResult() 35 | } 36 | 37 | // TODO doc 38 | func ShowSaveFileDialog(config cfd.DialogConfig) (string, error) { 39 | dialog, err := cfd.NewSaveFileDialog(config) 40 | if err != nil { 41 | return "", err 42 | } 43 | defer dialog.Release() 44 | return dialog.ShowAndGetResult() 45 | } 46 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/harry1453/go-common-file-dialog 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/go-ole/go-ole v1.3.0 7 | github.com/google/uuid v1.1.1 8 | ) 9 | 10 | require golang.org/x/sys v0.1.0 // indirect 11 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= 2 | github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= 3 | github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= 4 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 5 | golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= 6 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 7 | -------------------------------------------------------------------------------- /util/util.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "github.com/go-ole/go-ole" 5 | "github.com/google/uuid" 6 | ) 7 | 8 | func StringToUUID(str string) *ole.GUID { 9 | return ole.NewGUID(uuid.NewSHA1(uuid.Nil, []byte(str)).String()) 10 | } 11 | -------------------------------------------------------------------------------- /util/util_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "github.com/go-ole/go-ole" 5 | "testing" 6 | ) 7 | 8 | func TestStringToUUID(t *testing.T) { 9 | generated := *StringToUUID("TestTestTest") 10 | expected := *ole.NewGUID("7933985F-2C87-5A5B-A26E-5D0326829AC2") 11 | if generated != expected { 12 | t.Errorf("not equal. expected %s, found %s", expected.String(), generated.String()) 13 | } 14 | } 15 | --------------------------------------------------------------------------------