├── .github └── workflows │ └── main.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── examples ├── custom_file_types.rs ├── defaults.rs ├── folder_picker.rs ├── full.rs ├── open_multiple_selection.rs └── owner.rs └── src └── lib.rs /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | lint: 7 | runs-on: windows-latest 8 | 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v1 12 | 13 | - name: Clippy 14 | run: cargo clippy --all-targets --all-features -- -D warnings 15 | 16 | build_stable-msvc: 17 | runs-on: windows-latest 18 | 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v1 22 | 23 | - name: Build (stable-msvc) 24 | run: cargo +stable-msvc build 25 | 26 | build_stable-gnu: 27 | runs-on: windows-latest 28 | 29 | steps: 30 | - name: Checkout 31 | uses: actions/checkout@v1 32 | 33 | - name: Install Rust stable-gnu 34 | run: rustup toolchain install --no-self-update stable-gnu 35 | 36 | - name: Build (stable-gnu) 37 | run: cargo +stable-gnu build 38 | 39 | build_nightly-msvc: 40 | runs-on: windows-latest 41 | 42 | steps: 43 | - name: Checkout 44 | uses: actions/checkout@v1 45 | 46 | - name: Install Rust (nightly-msvc) 47 | run: rustup toolchain install --no-self-update nightly-msvc 48 | 49 | - name: Build (nightly-msvc) 50 | run: cargo +nightly-msvc build 51 | 52 | build_nightly-gnu: 53 | runs-on: windows-latest 54 | 55 | steps: 56 | - name: Checkout 57 | uses: actions/checkout@v1 58 | 59 | - name: Install Rust (nightly-gnu) 60 | run: rustup toolchain install --no-self-update nightly-gnu 61 | 62 | - name: Build (nightly-gnu) 63 | run: cargo +nightly-gnu build -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | /.vs/ 4 | /.idea/ 5 | gdb.exe.stackdump -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | [[package]] 4 | name = "libc" 5 | version = "0.2.64" 6 | source = "registry+https://github.com/rust-lang/crates.io-index" 7 | checksum = "74dfca3d9957906e8d1e6a0b641dc9a59848e793f1da2165889fd4f62d10d79c" 8 | 9 | [[package]] 10 | name = "wfd" 11 | version = "0.1.7" 12 | dependencies = [ 13 | "libc", 14 | "winapi", 15 | ] 16 | 17 | [[package]] 18 | name = "winapi" 19 | version = "0.3.9" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 22 | dependencies = [ 23 | "winapi-i686-pc-windows-gnu", 24 | "winapi-x86_64-pc-windows-gnu", 25 | ] 26 | 27 | [[package]] 28 | name = "winapi-i686-pc-windows-gnu" 29 | version = "0.4.0" 30 | source = "registry+https://github.com/rust-lang/crates.io-index" 31 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 32 | 33 | [[package]] 34 | name = "winapi-x86_64-pc-windows-gnu" 35 | version = "0.4.0" 36 | source = "registry+https://github.com/rust-lang/crates.io-index" 37 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 38 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wfd" 3 | version = "0.1.7" 4 | authors = ["Ben Wallis "] 5 | description = "A simple to use abstraction over the Open and Save dialogs in the Windows API" 6 | edition = "2018" 7 | license = "MIT" 8 | repository = "https://www.github.com/ben-wallis/wfd" 9 | readme = "README.md" 10 | keywords = ["file", "open", "save", "dialog", "windows"] 11 | categories = ["os::windows-apis"] 12 | exclude = [".github/*"] 13 | 14 | [package.metadata.docs.rs] 15 | default-target = "x86_64-pc-windows-msvc" 16 | 17 | [dependencies] 18 | winapi = { version = ">=0.3.9", features = ["winuser", "objbase", "shobjidl", "shobjidl_core", "winerror", "shellapi"] } 19 | libc = "0.2" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 Ben Wallis 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to use, 6 | copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the 7 | Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 14 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 15 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 16 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 17 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 18 | OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wfd 2 | [![Build Status](https://github.com/ben-wallis/wfd/workflows/Build/badge.svg)](https://github.com/ben-wallis/wfd/actions) 3 | [![Crates.io](https://img.shields.io/crates/v/wfd)](https://crates.io/crates/wfd) 4 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 5 | 6 | This crate provides a simple to use abstraction over the Open and Save dialogs in the Windows API, usable under both GNU and MSVC toolchains, with minimal dependencies. 7 | 8 | ## Examples 9 | 10 | ### Standard open dialog 11 | ```rust 12 | let dialog_result = wfd::open_dialog(Default::default())?; 13 | ``` 14 | 15 | ### Folder picker open dialog 16 | ```rust 17 | use wfd::{DialogParams}; 18 | 19 | let params = DialogParams { 20 | options: FOS_PICKFOLDERS, 21 | .. Default::default() 22 | }; 23 | 24 | let dialog_result = wfd::open_dialog(params)?; 25 | ``` 26 | 27 | ### Save dialog with custom file extension filters 28 | ```rust 29 | use wfd::{DialogParams}; 30 | 31 | let params = DialogParams { 32 | title: "Select an image to open", 33 | file_types: vec![("JPG Files", "*.jpg;*.jpeg"), ("PNG Files", "*.png"), ("Bitmap Files", "*.bmp")], 34 | default_extension: "jpg", 35 | ..Default::default() 36 | }; 37 | 38 | let dialog_result = wfd::save_dialog(params)?; 39 | ``` 40 | 41 | **Further examples can be found in `src\examples`** 42 | -------------------------------------------------------------------------------- /examples/custom_file_types.rs: -------------------------------------------------------------------------------- 1 | extern crate wfd; 2 | 3 | use wfd::DialogParams; 4 | 5 | fn main() { 6 | let params = DialogParams { 7 | title: "Select an image to open", 8 | file_types: vec![("JPG Files", "*.jpg;*.jpeg"), ("PNG Files", "*.png"), ("Bitmap Files", "*.bmp")], 9 | // Default to PNG Files 10 | file_type_index: 2, 11 | // Specifies the default extension before the user changes the File Type dropdown. Note that 12 | // omitting this field will result in no extension ever being appended to a filename that a 13 | // user types in even after they change the selected File Type. 14 | default_extension: "png", 15 | ..Default::default() 16 | }; 17 | 18 | let result = wfd::open_dialog(params); 19 | println!("{:?}", result); 20 | } -------------------------------------------------------------------------------- /examples/defaults.rs: -------------------------------------------------------------------------------- 1 | extern crate wfd; 2 | 3 | fn main() { 4 | let result = wfd::open_dialog(Default::default()); 5 | println!("{:?}", result); 6 | 7 | let result = wfd::save_dialog(Default::default()); 8 | println!("{:?}", result); 9 | } -------------------------------------------------------------------------------- /examples/folder_picker.rs: -------------------------------------------------------------------------------- 1 | extern crate wfd; 2 | 3 | use wfd::{DialogParams, FOS_PICKFOLDERS}; 4 | 5 | fn main() { 6 | let params = DialogParams { 7 | options: FOS_PICKFOLDERS, 8 | title: "Select a directory", 9 | ..Default::default() 10 | }; 11 | 12 | let result = wfd::open_dialog(params); 13 | println!("{:?}", result); 14 | } -------------------------------------------------------------------------------- /examples/full.rs: -------------------------------------------------------------------------------- 1 | extern crate wfd; 2 | 3 | use wfd::{DialogError, DialogParams, FOS_ALLOWMULTISELECT}; 4 | 5 | fn main() { 6 | let params = DialogParams { 7 | file_types: vec![("DLL Files", "*.dll"), ("Executable Files", "*.exe;*.com;*.scr")], 8 | default_extension: "dll", 9 | default_folder: r"C:\Windows\System32", 10 | file_name: "win32k.sys", 11 | file_name_label: "Select some files!", 12 | file_type_index: 1, 13 | ok_button_label: "Custom OK", 14 | options: FOS_ALLOWMULTISELECT, 15 | title: "Test open file dialog", 16 | ..Default::default() 17 | }; 18 | 19 | match wfd::open_dialog(params) { 20 | Ok(r) => { 21 | for file in r.selected_file_paths { 22 | println!("{}", file.to_str().unwrap()); 23 | } 24 | } 25 | Err(e) => match e { 26 | DialogError::HResultFailed { hresult, error_method} => { 27 | println!("HResult Failed - HRESULT: {:X}, Method: {}", hresult, error_method); 28 | } 29 | DialogError::UnsupportedFilepath => { println!("Unsupported file path"); } 30 | DialogError::UserCancelled => { 31 | println!("User cancelled dialog"); 32 | } 33 | }, 34 | } 35 | } -------------------------------------------------------------------------------- /examples/open_multiple_selection.rs: -------------------------------------------------------------------------------- 1 | extern crate wfd; 2 | 3 | use wfd::{DialogParams, FOS_ALLOWMULTISELECT}; 4 | 5 | fn main() { 6 | let params = DialogParams { 7 | options: FOS_ALLOWMULTISELECT, 8 | title: "Select multiple files to open", 9 | ..Default::default() 10 | }; 11 | 12 | let result = wfd::open_dialog(params); 13 | println!("{:?}", result); 14 | } -------------------------------------------------------------------------------- /examples/owner.rs: -------------------------------------------------------------------------------- 1 | extern crate wfd; 2 | 3 | use wfd::{DialogParams, FOS_PICKFOLDERS, HWND}; 4 | 5 | fn main() { 6 | // Replace this with a real HWND to test 7 | let hwnd = 0xdeadbeef as HWND; 8 | 9 | let params = DialogParams { 10 | options: FOS_PICKFOLDERS, 11 | owner: Some(hwnd), 12 | title: "Select a directory", 13 | ..Default::default() 14 | }; 15 | 16 | let result = wfd::open_dialog(params); 17 | println!("{:?}", result); 18 | } -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! This crate provides safe methods for using Open and Save dialog boxes on Windows. 2 | extern crate libc; 3 | extern crate winapi; 4 | 5 | use crate::winapi::Interface; 6 | 7 | use std::ffi::OsString; 8 | use std::os::windows::ffi::OsStringExt; 9 | use std::path::PathBuf; 10 | use std::ptr::null_mut; 11 | use std::slice; 12 | 13 | use libc::wcslen; 14 | use winapi::{ 15 | ctypes::c_void, 16 | shared::{ 17 | minwindef::LPVOID, 18 | ntdef::LPWSTR, 19 | winerror::{HRESULT, SUCCEEDED} 20 | }, 21 | um:: { 22 | combaseapi::{CoCreateInstance, CoInitializeEx, CoTaskMemFree, CoUninitialize, CLSCTX_ALL}, 23 | objbase::{COINIT_APARTMENTTHREADED, COINIT_DISABLE_OLE1DDE}, 24 | shobjidl::{IFileDialog, IFileOpenDialog, IFileSaveDialog, IShellItemArray}, 25 | shobjidl_core::{CLSID_FileOpenDialog, CLSID_FileSaveDialog, IShellItem, SFGAOF, SHCreateItemFromParsingName, SIGDN_FILESYSPATH}, 26 | shtypes::COMDLG_FILTERSPEC, 27 | } 28 | }; 29 | 30 | // Re-exports 31 | pub use winapi::um::shobjidl::{ 32 | FOS_ALLNONSTORAGEITEMS, FOS_ALLOWMULTISELECT, FOS_CREATEPROMPT, FOS_DEFAULTNOMINIMODE, 33 | FOS_DONTADDTORECENT, FOS_FILEMUSTEXIST, FOS_FORCEFILESYSTEM, FOS_FORCEPREVIEWPANEON, 34 | FOS_FORCESHOWHIDDEN, FOS_HIDEMRUPLACES, FOS_HIDEPINNEDPLACES, FOS_NOCHANGEDIR, 35 | FOS_NODEREFERENCELINKS, FOS_NOREADONLYRETURN, FOS_NOTESTFILECREATE, FOS_NOVALIDATE, 36 | FOS_OVERWRITEPROMPT, FOS_PATHMUSTEXIST, FOS_PICKFOLDERS, FOS_SHAREAWARE, FOS_STRICTFILETYPES, 37 | FOS_SUPPORTSTREAMABLEITEMS, 38 | }; 39 | pub use winapi::shared::windef::HWND; 40 | 41 | macro_rules! com { 42 | ($com_expr:expr, $method_name:expr ) => { com(|| unsafe { $com_expr }, $method_name) }; 43 | } 44 | 45 | trait NullTermUTF16 { 46 | fn as_null_term_utf16(&self) -> Vec; 47 | } 48 | 49 | impl NullTermUTF16 for str { 50 | fn as_null_term_utf16(&self) -> Vec { 51 | self.encode_utf16().chain(Some(0)).collect() 52 | } 53 | } 54 | 55 | const SFGAO_FILESYSTEM: u32 = 0x4000_0000; 56 | 57 | type FileExtensionFilterPair<'a> = (&'a str, &'a str); 58 | 59 | /// The parameters used when displaying a dialog box. All fields are optional and have appropriate 60 | /// default values 61 | #[derive(Debug)] 62 | pub struct DialogParams<'a> { 63 | /// The default file extension to add to the returned file name when a file extension 64 | /// is not entered. Note that if this is not set no extensions will be present on returned 65 | /// filenames even when a specific file type filter is selected. 66 | pub default_extension: &'a str, 67 | /// The path to the default folder that the dialog will navigate to on first usage. Subsequent 68 | /// usages of the dialog will remember the directory of the last selected file/folder. 69 | pub default_folder: &'a str, 70 | /// The filename to pre-populate in the dialog box 71 | pub file_name: &'a str, 72 | /// The label to display to the left of the filename input box in the dialog 73 | pub file_name_label: &'a str, 74 | /// Specifies the (1-based) index of the file type that is selected by default. 75 | pub file_type_index: u32, 76 | /// The file types that are displayed in the File Type dropdown box in the dialog. The first 77 | /// element is the text description, i.e `"Text Files (*.txt)"` and the second element is the 78 | /// file extension filter pattern, with multiple entries separated by a semi-colon 79 | /// i.e `"*.txt;*.log"` 80 | pub file_types: Vec<(&'a str, &'a str)>, 81 | /// The path to the folder that is always selected when a dialog is opened, regardless of 82 | /// previous user action. This is not recommended for general use, instead `default_folder` 83 | /// should be used. 84 | pub folder: &'a str, 85 | /// The text label to replace the default "Open" or "Save" text on the "OK" button of the dialog 86 | pub ok_button_label: &'a str, 87 | /// A set of bit flags to apply to the dialog. Setting invalid flags will result in the dialog 88 | /// failing to open. Flags should be a combination of `FOS_*` constants, the documentation for 89 | /// which can be found [here](https://docs.microsoft.com/en-us/windows/win32/api/shobjidl_core/ne-shobjidl_core-_fileopendialogoptions) 90 | pub options: u32, 91 | /// The HWND of the window that the dialog will be owned by. If not provided the dialog will be 92 | /// an independent top-level window. 93 | pub owner: Option, 94 | /// The path to the existing file to use when opening a Save As dialog. Acts as a combination of 95 | /// `folder` and `file_name`, displaying the file name in the edit box, and selecting the 96 | /// containing folder as the initial folder in the dialog. 97 | pub save_as_item: &'a str, 98 | /// The text displayed in the title bar of the dialog box 99 | pub title: &'a str 100 | } 101 | 102 | impl<'a> Default for DialogParams<'a> { 103 | fn default() -> Self { 104 | DialogParams { 105 | default_extension: "", 106 | default_folder: "", 107 | file_name: "", 108 | file_name_label: "", 109 | file_type_index: 1, 110 | file_types: vec![("All types (*.*)", "*.*")], 111 | folder: "", 112 | ok_button_label: "", 113 | options: 0, 114 | owner: None, 115 | save_as_item: "", 116 | title: "", 117 | } 118 | } 119 | } 120 | 121 | /// The result of an Open Dialog after the user has selected one or more files (or a folder) 122 | #[derive(Debug)] 123 | pub struct OpenDialogResult { 124 | /// The first file path that the user selected. Provided as a convenience for use when 125 | /// `FOS_ALLOWMULTISELECT` is not enabled. If multiple files are selected this field contains 126 | /// the first selected file path. 127 | pub selected_file_path: PathBuf, 128 | /// The file paths that the user selected. Will only ever contain a single file path if 129 | /// `FOS_ALLOWMULTISELECT` is not enabled. 130 | pub selected_file_paths: Vec, 131 | /// The 1-based index of the file type that was selected in the File Type dropdown 132 | pub selected_file_type_index: u32, 133 | } 134 | 135 | /// The result of a Save Dialog after the user has selected a file 136 | #[derive(Debug)] 137 | pub struct SaveDialogResult { 138 | /// The file path that the user selected 139 | pub selected_file_path: PathBuf, 140 | /// The 1-based index of the file type that was selected in the File Type dropdown 141 | pub selected_filter_index: u32, 142 | } 143 | 144 | /// Error returned when showing a dialog fails 145 | #[derive(Debug)] 146 | pub enum DialogError { 147 | /// The user cancelled the dialog 148 | UserCancelled, 149 | /// The filepath of the selected folder or item is not supported. This occurs when the selected path 150 | /// does not have the SFGAO_FILESYSTEM attribute. Selecting items without a regular filesystem path 151 | /// such as "This Computer" or a file or folder within a WPD device like a phone will cause this error. 152 | UnsupportedFilepath, 153 | /// An error occurred when showing the dialog, the HRESULT that caused the error is included. 154 | /// This error most commonly occurs when invalid combinations of parameters are provided 155 | HResultFailed { 156 | /// The COM method that failed 157 | error_method: String, 158 | /// The HRESULT error code 159 | hresult: i32 160 | }, 161 | } 162 | 163 | /// Displays an Open Dialog using the provided parameters. 164 | /// 165 | /// # Examples 166 | /// 167 | /// ``` 168 | /// // An entirely default Open File dialog box with no customization 169 | /// let result = wfd::open_dialog(Default::default()); 170 | /// ``` 171 | /// ``` 172 | /// // A folder-picker Open dialog box with a custom dialog title 173 | /// # use std::io; 174 | /// # fn main() -> Result<(), wfd::DialogError> { 175 | /// let params = wfd::DialogParams { 176 | /// options: wfd::FOS_PICKFOLDERS, 177 | /// title: "My custom open folder dialog", 178 | /// ..Default::default() 179 | /// }; 180 | /// let result = wfd::open_dialog(params)?; 181 | /// let path = result.selected_file_path; 182 | /// # Ok(()) 183 | /// # } 184 | /// ``` 185 | /// ``` 186 | /// // An Open dialog box with a custom dialog title and file types 187 | /// # use std::io; 188 | /// # fn main() -> Result<(), wfd::DialogError> { 189 | /// let params = wfd::DialogParams { 190 | /// title: "My custom open file dialog", 191 | /// file_types: vec![("JPG Files", "*.jpg;*.jpeg"), ("PDF Files", "*.pdf")], 192 | /// ..Default::default() 193 | /// }; 194 | /// let result = wfd::open_dialog(params)?; 195 | /// let path = result.selected_file_path; 196 | /// # Ok(()) 197 | /// # } 198 | /// ``` 199 | /// 200 | /// # Errors 201 | /// If a user cancels the dialog, the [`UserCancelled`] error is returned. The only other kinds of 202 | /// errors that can be retured are COM [`HRESULT`] failure codes - usually as the result of invalid 203 | /// combinations of options. These are returned in a [`HResultFailed`] error 204 | /// 205 | /// [`UserCancelled`]: enum.FileDialogError.html#variant.UserCancelled 206 | /// [`HResultFailed`]: enum.FileDialogError.html#variant.HResultFailed 207 | /// [`HRESULT`]: https://en.wikipedia.org/wiki/HRESULT 208 | pub fn open_dialog(params: DialogParams) -> Result { 209 | // Initialize COM 210 | com!(CoInitializeEx( 211 | null_mut(), 212 | COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE, 213 | ), "CoInitializeEx")?; 214 | 215 | // Create IFileOpenDialog instance 216 | let mut file_open_dialog: *mut IFileOpenDialog = null_mut(); 217 | com!(CoCreateInstance( 218 | &CLSID_FileOpenDialog, 219 | null_mut(), 220 | CLSCTX_ALL, 221 | &IFileOpenDialog::uuidof(), 222 | &mut file_open_dialog as *mut *mut IFileOpenDialog as *mut *mut c_void, 223 | ), "CoCreateInstance - IFileOpenDialog")?; 224 | let file_open_dialog = unsafe { &*file_open_dialog }; 225 | 226 | // Perform non open-specific dialog configuration 227 | configure_file_dialog(file_open_dialog, ¶ms)?; 228 | 229 | show_dialog(file_open_dialog, params.owner)?; 230 | 231 | // Get the item(s) that the user selected in the dialog 232 | // IFileOpenDialog::GetResults 233 | let mut shell_item_array: *mut IShellItemArray = null_mut(); 234 | com!(file_open_dialog.GetResults(&mut shell_item_array), "IFileOpenDialog::GetResults")?; 235 | 236 | let shell_item_array = unsafe { &*shell_item_array }; 237 | 238 | // IShellItemArray::GetCount 239 | let mut item_count: u32 = 0; 240 | com!(shell_item_array.GetCount(&mut item_count), "IShellItemArray::GetCount")?; 241 | 242 | let mut file_paths: Vec = vec![]; 243 | for i in 0..item_count { 244 | // IShellItemArray::GetItemAt 245 | let mut shell_item: *mut IShellItem = null_mut(); 246 | com!(shell_item_array.GetItemAt(i, &mut shell_item), "IShellItemArray::GetItemAt")?; 247 | let shell_item = unsafe { &*shell_item }; 248 | 249 | // Fetch the SFGAO_FILESYSTEM attribute for the file 250 | let mut attribs: SFGAOF = 0; 251 | // IShellItem::GetAttributes 252 | com!(shell_item.GetAttributes(SFGAO_FILESYSTEM, &mut attribs), "IShellItem::GetAttributes")?; 253 | 254 | // Ignore shell items that do not have the SFGAO_FILESYSTEM attribute 255 | // which indicates that they represent a valid path to a file or folder 256 | if attribs & SFGAO_FILESYSTEM == 0 { 257 | continue; 258 | } 259 | 260 | let file_name = get_shell_item_display_name(&shell_item)?; 261 | file_paths.push(PathBuf::from(file_name)); 262 | 263 | // Free non-owned allocation 264 | unsafe { shell_item.Release() }; 265 | } 266 | 267 | // IFileDialog::GetFileTypeIndex 268 | let selected_filter_index = get_file_type_index(file_open_dialog)?; 269 | 270 | // Un-initialize COM 271 | unsafe { 272 | CoUninitialize(); 273 | } 274 | 275 | file_paths.get(0).cloned().map(|x| { 276 | OpenDialogResult { 277 | selected_file_path: x, 278 | selected_file_paths: file_paths, 279 | selected_file_type_index: selected_filter_index 280 | } 281 | }).ok_or(DialogError::UnsupportedFilepath) 282 | } 283 | 284 | /// Displays a Save Dialog using the provided parameters. 285 | /// 286 | /// # Examples 287 | /// 288 | /// ``` 289 | /// # fn main() -> Result<(), wfd::DialogError> { 290 | /// // An entirely default Save File dialog box with no customization 291 | /// let result = wfd::save_dialog(Default::default())?; 292 | /// # Ok(()) 293 | /// # } 294 | /// ``` 295 | /// ``` 296 | /// # fn main() -> Result<(), wfd::DialogError> { 297 | /// // A Save File dialog box with a custom dialog title and file types/// 298 | /// let params = wfd::DialogParams { 299 | /// title: "My custom save file dialog", 300 | /// file_types: vec![("JPG Files", "*.jpg;*.jpeg"), ("PDF Files", "*.pdf")], 301 | /// ..Default::default() 302 | /// }; 303 | /// let result = wfd::save_dialog(params)?; 304 | /// # Ok(()) 305 | /// # } 306 | /// ``` 307 | /// 308 | /// # Errors 309 | /// If a user cancels the dialog, the [`UserCancelled`] error is returned. The only other kinds of 310 | /// errors that can be retured are COM [`HRESULT`] failure codes - usually as the result of invalid 311 | /// combinations of options. These are returned in a [`HResultFailed`] error 312 | /// 313 | /// [`UserCancelled`]: enum.FileDialogError.html#variant.UserCancelled 314 | /// [`HResultFailed`]: enum.FileDialogError.html#variant.HResultFailed 315 | /// [`HRESULT`]: https://en.wikipedia.org/wiki/HRESULT 316 | pub fn save_dialog(params: DialogParams) -> Result { 317 | // Initialize COM 318 | com!(CoInitializeEx( 319 | null_mut(), 320 | COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE, 321 | ) 322 | , "CoInitializeEx")?; 323 | 324 | // Create IFileSaveDialog instance 325 | let mut file_save_dialog: *mut IFileSaveDialog; 326 | file_save_dialog = null_mut(); 327 | com!(CoCreateInstance( 328 | &CLSID_FileSaveDialog, 329 | null_mut(), 330 | CLSCTX_ALL, 331 | &IFileSaveDialog::uuidof(), 332 | &mut file_save_dialog as *mut *mut IFileSaveDialog as *mut *mut c_void, 333 | ) 334 | , "CoCreateInstance - FileSaveDialog")?; 335 | let file_save_dialog = unsafe { &*file_save_dialog }; 336 | 337 | // IFileDialog::SetSaveAsItem 338 | if params.save_as_item != "" { 339 | let mut item: *mut IShellItem = null_mut(); 340 | let path = params.save_as_item.as_null_term_utf16(); 341 | com!(SHCreateItemFromParsingName(path.as_ptr(), null_mut(), &IShellItem::uuidof(), &mut item as *mut *mut IShellItem as *mut *mut c_void), "SHCreateItemFromParsingName")?; 342 | com!(file_save_dialog.SetSaveAsItem(item), "IFileDialog::SetSaveAsItem")?; 343 | unsafe { 344 | let item = &*item; 345 | item.Release(); 346 | } 347 | } 348 | 349 | // Perform non save-specific dialog configuration 350 | configure_file_dialog(file_save_dialog, ¶ms)?; 351 | 352 | show_dialog(file_save_dialog, params.owner)?; 353 | 354 | // IFileDialog::GetResult 355 | let mut shell_item: *mut IShellItem = null_mut(); 356 | com!(file_save_dialog.GetResult(&mut shell_item), "IFileDialog::GetResult")?; 357 | let shell_item = unsafe { &*shell_item }; 358 | let file_name = get_shell_item_display_name(&shell_item)?; 359 | unsafe { shell_item.Release() }; 360 | 361 | // IFileDialog::GetFileTypeIndex 362 | let selected_filter_index = get_file_type_index(file_save_dialog)?; 363 | 364 | // Un-initialize COM 365 | unsafe { 366 | CoUninitialize(); 367 | } 368 | 369 | let result = SaveDialogResult { 370 | selected_filter_index, 371 | selected_file_path: PathBuf::from(file_name), 372 | }; 373 | 374 | Ok(result) 375 | } 376 | 377 | #[allow(overflowing_literals)] 378 | #[allow(unused_comparisons)] 379 | fn show_dialog(file_dialog: &IFileDialog, owner: Option) -> Result<(), DialogError> { 380 | let owner_hwnd = owner.unwrap_or(null_mut()); 381 | 382 | // IModalWindow::Show 383 | let result = com!(file_dialog.Show(owner_hwnd), "IModalWindow::Show"); 384 | 385 | match result { 386 | Ok(_) => Ok(()), 387 | Err(e) => match e { 388 | DialogError::HResultFailed { hresult, .. } => { 389 | if hresult == 0x8007_04C7 { 390 | Err(DialogError::UserCancelled) 391 | } else { 392 | Err(e) 393 | } 394 | } 395 | _ => Err(e), 396 | }, 397 | } 398 | } 399 | 400 | fn configure_file_dialog(file_dialog: &IFileDialog, params: &DialogParams) -> Result<(), DialogError> { 401 | // IFileDialog::SetDefaultExtension 402 | if params.default_extension != "" { 403 | let default_extension = params.default_extension.as_null_term_utf16(); 404 | com!(file_dialog.SetDefaultExtension(default_extension.as_ptr()), "IFileDialog::SetDefaultExtension")?; 405 | } 406 | 407 | // IFileDialog::SetDefaultFolder 408 | if params.default_folder != "" { 409 | let mut default_folder: *mut IShellItem = null_mut(); 410 | let path = params.default_folder.as_null_term_utf16(); 411 | com!(SHCreateItemFromParsingName(path.as_ptr(), null_mut(), &IShellItem::uuidof(), &mut default_folder as *mut *mut IShellItem as *mut *mut c_void), "SHCreateItemFromParsingName")?; 412 | com!(file_dialog.SetDefaultFolder(default_folder), "IFileDialog::SetDefaultFolder")?; 413 | unsafe { 414 | let default_folder = &*default_folder; 415 | default_folder.Release(); 416 | } 417 | } 418 | 419 | // IFileDialog::SetFolder 420 | if params.folder != "" { 421 | let mut folder: *mut IShellItem = null_mut(); 422 | let path = params.folder.as_null_term_utf16(); 423 | com!(SHCreateItemFromParsingName(path.as_ptr(), null_mut(), &IShellItem::uuidof(), &mut folder as *mut *mut IShellItem as *mut *mut c_void), "SHCreateItemFromParsingName")?; 424 | com!(file_dialog.SetFolder(folder), "IFileDialog::SetFolder")?; 425 | unsafe { 426 | let folder = &*folder; 427 | folder.Release(); 428 | } 429 | } 430 | 431 | // IFileDialog::SetFileName 432 | if params.file_name != "" { 433 | let initial_file_name = params.file_name.as_null_term_utf16(); 434 | com!(file_dialog.SetFileName(initial_file_name.as_ptr()), "IFileDialog::SetFileName")?; 435 | } 436 | 437 | // IFileDialog::SetFileNameLabel 438 | if params.file_name_label != "" { 439 | let file_name_label = params.file_name_label.as_null_term_utf16(); 440 | com!(file_dialog.SetFileNameLabel(file_name_label.as_ptr()), "IFileDialog::SetFileNameLabel")?; 441 | } 442 | 443 | if !params.file_types.is_empty() { 444 | add_filters(file_dialog, ¶ms.file_types)?; 445 | } 446 | 447 | // IFileDialog::SetFileTypeIndex 448 | if !params.file_types.is_empty() && params.file_type_index > 0 { 449 | com!(file_dialog.SetFileTypeIndex(params.file_type_index), "IFileDialog::SetFileTypeIndex")?; 450 | } 451 | 452 | // IFileDialog::SetOkButtonLabel 453 | if params.ok_button_label != "" { 454 | let ok_buttom_label = params.ok_button_label.as_null_term_utf16(); 455 | com!(file_dialog.SetOkButtonLabel(ok_buttom_label.as_ptr()), "IFileDialog::SetOkButtonLabel")?; 456 | } 457 | 458 | if params.options > 0 { 459 | // IFileDialog::GetOptions 460 | let mut existing_options: u32 = 0; 461 | com!(file_dialog.GetOptions(&mut existing_options), "IFileDialog::GetOptions")?; 462 | 463 | // IFileDialog::SetOptions 464 | com!(file_dialog.SetOptions(existing_options | params.options), "IFileDialog::SetOptions")?; 465 | } 466 | 467 | // IFileDialog::SetTitle 468 | if params.title != "" { 469 | let title = params.title.as_null_term_utf16(); 470 | com!(file_dialog.SetTitle(title.as_ptr()), "IFileDialog::SetTitle")?; 471 | } 472 | 473 | Ok(()) 474 | } 475 | 476 | fn add_filters(dialog: &IFileDialog, filters: &[FileExtensionFilterPair]) -> Result<(), DialogError> { 477 | // Create a vec holding the UTF-16 string pairs for the filter - we need 478 | // to have these in a vec since we need to be able to pass a pointer to them 479 | // in the COMDLG_FILTERSPEC structs passed to SetFileTypes. 480 | let temp_filters = filters 481 | .iter() 482 | .map(|filter| { 483 | let name = filter.0.as_null_term_utf16(); 484 | let pattern = filter.1.as_null_term_utf16(); 485 | (name, pattern) 486 | }) 487 | .collect::, Vec)>>(); 488 | 489 | let filter_specs = temp_filters 490 | .iter() 491 | .map(|x| COMDLG_FILTERSPEC { 492 | pszName: x.0.as_ptr(), 493 | pszSpec: x.1.as_ptr(), 494 | }) 495 | .collect::>(); 496 | 497 | // IFileDialog::SetFileTypes 498 | com!(dialog.SetFileTypes(filter_specs.len() as u32, filter_specs.as_ptr()), "IFileDialog::SetFileTypes")?; 499 | 500 | Ok(()) 501 | } 502 | 503 | fn get_file_type_index(file_dialog: &IFileDialog) -> Result { 504 | // IFileDialog::GetFileTypeIndex 505 | let mut selected_filter_index: u32 = 0; 506 | com!(file_dialog.GetFileTypeIndex(&mut selected_filter_index), "IFileDialog::GetFileTypeIndex")?; 507 | Ok(selected_filter_index) 508 | } 509 | 510 | fn get_shell_item_display_name(shell_item: &IShellItem) -> Result { 511 | let mut display_name: LPWSTR = null_mut(); 512 | // IShellItem::GetDisplayName 513 | com!(shell_item.GetDisplayName(SIGDN_FILESYSPATH, &mut display_name), "IShellItem::GetDisplayName")?; 514 | let slice = unsafe { slice::from_raw_parts(display_name, wcslen(display_name)) }; 515 | let result = OsString::from_wide(slice); 516 | 517 | // Free non-owned allocation 518 | unsafe { CoTaskMemFree(display_name as LPVOID) }; 519 | 520 | Ok(result) 521 | } 522 | 523 | // This wrapper method makes working with COM methods much simpler by 524 | // returning Err if the HRESULT for a call does not return success. 525 | fn com(mut f: F, method: &str) -> Result<(), DialogError> 526 | where 527 | F: FnMut() -> HRESULT, 528 | { 529 | let hresult = f(); 530 | if !SUCCEEDED(hresult) { 531 | Err(DialogError::HResultFailed { 532 | hresult, 533 | error_method: method.to_string() }) 534 | } else { 535 | Ok(()) 536 | } 537 | } 538 | --------------------------------------------------------------------------------