├── .editorconfig ├── .gitattributes ├── .gitignore ├── LICENSE.txt ├── README.md ├── TaskDialog.Example ├── Program.cs ├── TaskDialog.Example.csproj └── app.manifest ├── TaskDialog.sln └── TaskDialog ├── TaskDialog.WindowSubclassHandler.cs ├── TaskDialog.cs ├── TaskDialog.csproj ├── TaskDialogButton.cs ├── TaskDialogButtonClickedEventArgs.cs ├── TaskDialogButtons.cs ├── TaskDialogCheckbox.cs ├── TaskDialogClosingEventArgs.cs ├── TaskDialogControl.cs ├── TaskDialogCustomButton.cs ├── TaskDialogCustomButtonCollection.cs ├── TaskDialogCustomButtonStyle.cs ├── TaskDialogExpander.cs ├── TaskDialogFooter.cs ├── TaskDialogHyperlinkClickedEventArgs.cs ├── TaskDialogIcon.cs ├── TaskDialogIconHandle.cs ├── TaskDialogNativeMethods.cs ├── TaskDialogPage.cs ├── TaskDialogProgressBar.cs ├── TaskDialogProgressBarState.cs ├── TaskDialogRadioButton.cs ├── TaskDialogRadioButtonCollection.cs ├── TaskDialogResult.cs ├── TaskDialogStandardButton.cs ├── TaskDialogStandardButtonCollection.cs ├── TaskDialogStandardIcon.cs ├── TaskDialogStandardIconContainer.cs ├── TaskDialogStartupLocation.cs ├── WindowSubclassHandler.cs └── WindowSubclassHandlerNativeMethods.cs /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | # Note: This file is copied from https://github.com/dotnet/winforms/blob/master/.editorconfig 4 | 5 | # top-most EditorConfig file 6 | root = true 7 | 8 | # Default settings: 9 | # A newline ending every file 10 | # Use 4 spaces as indentation 11 | [*] 12 | indent_style = space 13 | indent_size = 4 14 | 15 | [*.json] 16 | indent_size = 2 17 | 18 | # C# files 19 | [*.cs] 20 | charset = utf-8-bom 21 | insert_final_newline = true 22 | # New line preferences 23 | csharp_new_line_before_open_brace = all 24 | csharp_new_line_before_else = true 25 | csharp_new_line_before_catch = true 26 | csharp_new_line_before_finally = true 27 | csharp_new_line_before_members_in_object_initializers = true 28 | csharp_new_line_before_members_in_anonymous_types = true 29 | csharp_new_line_between_query_expression_clauses = true 30 | 31 | # Indentation preferences 32 | csharp_indent_block_contents = true 33 | csharp_indent_braces = false 34 | csharp_indent_case_contents = true 35 | csharp_indent_switch_labels = true 36 | csharp_indent_labels = one_less_than_current 37 | 38 | # avoid this. unless absolutely necessary 39 | dotnet_style_qualification_for_field = false:suggestion 40 | dotnet_style_qualification_for_property = false:suggestion 41 | dotnet_style_qualification_for_method = false:suggestion 42 | dotnet_style_qualification_for_event = false:suggestion 43 | 44 | # only use var when it's obvious what the variable type is 45 | csharp_style_var_for_built_in_types = false:none 46 | csharp_style_var_when_type_is_apparent = false:none 47 | csharp_style_var_elsewhere = false:suggestion 48 | 49 | # use language keywords instead of BCL types 50 | dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion 51 | dotnet_style_predefined_type_for_member_access = true:suggestion 52 | 53 | # name all constant fields using PascalCase 54 | dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion 55 | dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields 56 | dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style 57 | 58 | dotnet_naming_symbols.constant_fields.applicable_kinds = field 59 | dotnet_naming_symbols.constant_fields.required_modifiers = const 60 | 61 | dotnet_naming_style.pascal_case_style.capitalization = pascal_case 62 | 63 | # static fields should have s_ prefix 64 | dotnet_naming_rule.static_fields_should_have_prefix.severity = suggestion 65 | dotnet_naming_rule.static_fields_should_have_prefix.symbols = static_fields 66 | dotnet_naming_rule.static_fields_should_have_prefix.style = static_prefix_style 67 | 68 | dotnet_naming_symbols.static_fields.applicable_kinds = field 69 | dotnet_naming_symbols.static_fields.required_modifiers = static 70 | 71 | dotnet_naming_style.static_prefix_style.required_prefix = s_ 72 | dotnet_naming_style.static_prefix_style.capitalization = camel_case 73 | 74 | # Comment this group and uncomment out the next group if you don't want _ prefixed fields. 75 | 76 | # internal and private fields should be _camelCase 77 | #dotnet_naming_rule.camel_case_for_private_internal_fields.severity = suggestion 78 | #dotnet_naming_rule.camel_case_for_private_internal_fields.symbols = private_internal_fields 79 | #dotnet_naming_rule.camel_case_for_private_internal_fields.style = camel_case_underscore_style 80 | 81 | # internal and private fields should be _camelCase 82 | dotnet_naming_rule.camel_case_for_private_internal_fields.severity = suggestion 83 | dotnet_naming_rule.camel_case_for_private_internal_fields.symbols = private_internal_fields 84 | dotnet_naming_rule.camel_case_for_private_internal_fields.style = camel_case_underscore_style 85 | 86 | dotnet_naming_symbols.private_internal_fields.applicable_kinds = field 87 | dotnet_naming_symbols.private_internal_fields.applicable_accessibilities = private, internal 88 | 89 | dotnet_naming_style.camel_case_underscore_style.required_prefix = _ 90 | dotnet_naming_style.camel_case_underscore_style.capitalization = camel_case 91 | 92 | # Code style defaults 93 | dotnet_sort_system_directives_first = true 94 | csharp_preserve_single_line_blocks = true 95 | csharp_preserve_single_line_statements = false 96 | 97 | # Expression-level preferences 98 | dotnet_style_object_initializer = true:suggestion 99 | dotnet_style_collection_initializer = true:suggestion 100 | dotnet_style_explicit_tuple_names = true:suggestion 101 | dotnet_style_coalesce_expression = true:suggestion 102 | dotnet_style_null_propagation = true:suggestion 103 | 104 | # Expression-bodied members 105 | csharp_style_expression_bodied_methods = false:none 106 | csharp_style_expression_bodied_constructors = false:none 107 | csharp_style_expression_bodied_operators = false:none 108 | csharp_style_expression_bodied_properties = true:none 109 | csharp_style_expression_bodied_indexers = true:none 110 | csharp_style_expression_bodied_accessors = true:none 111 | 112 | # Pattern matching 113 | csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion 114 | csharp_style_pattern_matching_over_as_with_null_check = true:suggestion 115 | csharp_style_inlined_variable_declaration = true:suggestion 116 | 117 | # Null checking preferences 118 | csharp_style_throw_expression = true:suggestion 119 | csharp_style_conditional_delegate_call = true:suggestion 120 | 121 | # Space preferences 122 | csharp_space_after_cast = false 123 | csharp_space_after_colon_in_inheritance_clause = true 124 | csharp_space_after_comma = true 125 | csharp_space_after_dot = false 126 | csharp_space_after_keywords_in_control_flow_statements = true 127 | csharp_space_after_semicolon_in_for_statement = true 128 | csharp_space_around_binary_operators = before_and_after 129 | csharp_space_around_declaration_statements = do_not_ignore 130 | csharp_space_before_colon_in_inheritance_clause = true 131 | csharp_space_before_comma = false 132 | csharp_space_before_dot = false 133 | csharp_space_before_open_square_brackets = false 134 | csharp_space_before_semicolon_in_for_statement = false 135 | csharp_space_between_empty_square_brackets = false 136 | csharp_space_between_method_call_empty_parameter_list_parentheses = false 137 | csharp_space_between_method_call_name_and_opening_parenthesis = false 138 | csharp_space_between_method_call_parameter_list_parentheses = false 139 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false 140 | csharp_space_between_method_declaration_name_and_open_parenthesis = false 141 | csharp_space_between_method_declaration_parameter_list_parentheses = false 142 | csharp_space_between_parentheses = false 143 | csharp_space_between_square_brackets = false 144 | 145 | # C++ Files 146 | [*.{cpp,h,in}] 147 | curly_bracket_next_line = true 148 | indent_brace_style = Allman 149 | 150 | # Xml project files 151 | [*.{csproj,vcxproj,vcxproj.filters,proj,nativeproj,locproj}] 152 | indent_size = 2 153 | 154 | # Xml build files 155 | [*.builds] 156 | indent_size = 2 157 | 158 | # Xml files 159 | [*.{xml,stylecop,ruleset}] 160 | indent_size = 2 161 | 162 | # Xml config files 163 | [*.{props,targets,config,nuspec}] 164 | indent_size = 2 165 | 166 | # resx Files 167 | [*.{resx,xlf}] 168 | indent_size = 2 169 | charset = utf-8-bom 170 | insert_final_newline = true 171 | 172 | # Shell scripts 173 | [*.sh] 174 | end_of_line = lf 175 | [*.{cmd, bat}] 176 | end_of_line = crlf 177 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | project.fragment.lock.json 46 | artifacts/ 47 | 48 | *_i.c 49 | *_p.c 50 | *_i.h 51 | *.ilk 52 | *.meta 53 | *.obj 54 | *.pch 55 | *.pdb 56 | *.pgc 57 | *.pgd 58 | *.rsp 59 | *.sbr 60 | *.tlb 61 | *.tli 62 | *.tlh 63 | *.tmp 64 | *.tmp_proj 65 | *.log 66 | *.vspscc 67 | *.vssscc 68 | .builds 69 | *.pidb 70 | *.svclog 71 | *.scc 72 | 73 | # Chutzpah Test files 74 | _Chutzpah* 75 | 76 | # Visual C++ cache files 77 | ipch/ 78 | *.aps 79 | *.ncb 80 | *.opendb 81 | *.opensdf 82 | *.sdf 83 | *.cachefile 84 | *.VC.db 85 | *.VC.VC.opendb 86 | 87 | # Visual Studio profiler 88 | *.psess 89 | *.vsp 90 | *.vspx 91 | *.sap 92 | 93 | # TFS 2012 Local Workspace 94 | $tf/ 95 | 96 | # Guidance Automation Toolkit 97 | *.gpState 98 | 99 | # ReSharper is a .NET coding add-in 100 | _ReSharper*/ 101 | *.[Rr]e[Ss]harper 102 | *.DotSettings.user 103 | 104 | # JustCode is a .NET coding add-in 105 | .JustCode 106 | 107 | # TeamCity is a build add-in 108 | _TeamCity* 109 | 110 | # DotCover is a Code Coverage Tool 111 | *.dotCover 112 | 113 | # NCrunch 114 | _NCrunch_* 115 | .*crunch*.local.xml 116 | nCrunchTemp_* 117 | 118 | # MightyMoose 119 | *.mm.* 120 | AutoTest.Net/ 121 | 122 | # Web workbench (sass) 123 | .sass-cache/ 124 | 125 | # Installshield output folder 126 | [Ee]xpress/ 127 | 128 | # DocProject is a documentation generator add-in 129 | DocProject/buildhelp/ 130 | DocProject/Help/*.HxT 131 | DocProject/Help/*.HxC 132 | DocProject/Help/*.hhc 133 | DocProject/Help/*.hhk 134 | DocProject/Help/*.hhp 135 | DocProject/Help/Html2 136 | DocProject/Help/html 137 | 138 | # Click-Once directory 139 | publish/ 140 | 141 | # Publish Web Output 142 | *.[Pp]ublish.xml 143 | *.azurePubxml 144 | # TODO: Comment the next line if you want to checkin your web deploy settings 145 | # but database connection strings (with potential passwords) will be unencrypted 146 | #*.pubxml 147 | *.publishproj 148 | 149 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 150 | # checkin your Azure Web App publish settings, but sensitive information contained 151 | # in these scripts will be unencrypted 152 | PublishScripts/ 153 | 154 | # NuGet Packages 155 | *.nupkg 156 | # The packages folder can be ignored because of Package Restore 157 | **/packages/* 158 | # except build/, which is used as an MSBuild target. 159 | !**/packages/build/ 160 | # Uncomment if necessary however generally it will be regenerated when needed 161 | #!**/packages/repositories.config 162 | # NuGet v3's project.json files produces more ignoreable files 163 | *.nuget.props 164 | *.nuget.targets 165 | 166 | # Microsoft Azure Build Output 167 | csx/ 168 | *.build.csdef 169 | 170 | # Microsoft Azure Emulator 171 | ecf/ 172 | rcf/ 173 | 174 | # Windows Store app package directories and files 175 | AppPackages/ 176 | BundleArtifacts/ 177 | Package.StoreAssociation.xml 178 | _pkginfo.txt 179 | 180 | # Visual Studio cache files 181 | # files ending in .cache can be ignored 182 | *.[Cc]ache 183 | # but keep track of directories ending in .cache 184 | !*.[Cc]ache/ 185 | 186 | # Others 187 | ClientBin/ 188 | ~$* 189 | *~ 190 | *.dbmdl 191 | *.dbproj.schemaview 192 | *.jfm 193 | *.pfx 194 | *.publishsettings 195 | node_modules/ 196 | orleans.codegen.cs 197 | 198 | # Since there are multiple workflows, uncomment next line to ignore bower_components 199 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 200 | #bower_components/ 201 | 202 | # RIA/Silverlight projects 203 | Generated_Code/ 204 | 205 | # Backup & report files from converting an old project file 206 | # to a newer Visual Studio version. Backup files are not needed, 207 | # because we have git ;-) 208 | _UpgradeReport_Files/ 209 | Backup*/ 210 | UpgradeLog*.XML 211 | UpgradeLog*.htm 212 | 213 | # SQL Server files 214 | *.mdf 215 | *.ldf 216 | 217 | # Business Intelligence projects 218 | *.rdl.data 219 | *.bim.layout 220 | *.bim_*.settings 221 | 222 | # Microsoft Fakes 223 | FakesAssemblies/ 224 | 225 | # GhostDoc plugin setting file 226 | *.GhostDoc.xml 227 | 228 | # Node.js Tools for Visual Studio 229 | .ntvs_analysis.dat 230 | 231 | # Visual Studio 6 build log 232 | *.plg 233 | 234 | # Visual Studio 6 workspace options file 235 | *.opt 236 | 237 | # Visual Studio LightSwitch build output 238 | **/*.HTMLClient/GeneratedArtifacts 239 | **/*.DesktopClient/GeneratedArtifacts 240 | **/*.DesktopClient/ModelManifest.xml 241 | **/*.Server/GeneratedArtifacts 242 | **/*.Server/ModelManifest.xml 243 | _Pvt_Extensions 244 | 245 | # Paket dependency manager 246 | .paket/paket.exe 247 | paket-files/ 248 | 249 | # FAKE - F# Make 250 | .fake/ 251 | 252 | # JetBrains Rider 253 | .idea/ 254 | *.sln.iml 255 | 256 | # CodeRush 257 | .cr/ 258 | 259 | # Python Tools for Visual Studio (PTVS) 260 | __pycache__/ 261 | *.pyc -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 Konstantin Preißer, www.preisser-it.de 2 | 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | this software and associated documentation files (the "Software"), to deal in 6 | the Software without restriction, including without limitation the rights to 7 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 8 | of the Software, and to permit persons to whom the Software is furnished to do 9 | so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Task Dialog for .NET (Windows) (Archived) 2 | 3 | **Note:** This repository is now archived as the Task Dialog implementation has been merged into **.NET 5.0** with PR https://github.com/dotnet/winforms/pull/1133. 4 | You can now use the built-in class [`System.Windows.Forms.TaskDialog`](https://learn.microsoft.com/dotnet/api/system.windows.forms.taskdialog) to show a task dialog. 5 | 6 | --- 7 | 8 | The Task Dialog is the successor of a MessageBox and available starting with Windows Vista. For more information, 9 | see [About Task Dialogs](https://docs.microsoft.com/en-us/windows/desktop/Controls/task-dialogs-overview). 10 | 11 | This project aims to provide a complete .NET implementation (C#) of the Task Dialog with all the features that 12 | are also available in the native APIs, with all the marshalling and memory management done under the hood. 13 | 14 | The project targets .NET Framework 4.7.2 and .NET Standard 2.0. 15 | 16 | **Task Dialog Features:** 17 | * Supports all of the native Task Dialog elements (like custom buttons/command links, progress bar, radio buttons, checkbox, expanded area, footer) 18 | * Some dialog elements can be updated while the dialog is opened 19 | * Additionally to standard icons, supports security icons that show a green, yellow, red, gray or blue bar 20 | * Can navigate to a new page (by reconstructing the dialog from current properties) 21 | * Can be shown modal or non-modal 22 | * Exposes its window handle (`hWnd`) through the `Handle` property so that the dialog window can be further manipulated (or used as owner for another window) 23 | 24 | ![taskdialog-screenshot-1](https://user-images.githubusercontent.com/13289184/48280515-1b3a6e00-e454-11e8-96f3-b22a3bcff22e.png)   ![taskdialog-screenshot-2](https://user-images.githubusercontent.com/13289184/48280347-9cddcc00-e453-11e8-9bc1-605a55e8aaec.png) 25 | 26 | 27 | ## Prerequisites 28 | 29 | To use the Task Dialog, your application needs to be compiled with a manifest that contains a dependency to 30 | `Microsoft.Windows.Common-Controls` 6.0.0.0 (otherwise, an 31 | [`EntryPointNotFoundException`](https://docs.microsoft.com/dotnet/api/system.entrypointnotfoundexception) 32 | will occur when trying to show the dialog): 33 | ```xml 34 | 35 | 36 | 37 | 38 | 39 | 40 | 48 | 49 | 50 | 51 | ``` 52 | 53 | You can find a sample manifest file in the [`TaskDialog.Example`](/TaskDialog.Example) project. 54 | 55 | Also, please make sure your `Main()` method has the 56 | [`[STAThread]`](https://docs.microsoft.com/dotnet/api/system.stathreadattribute) attribute 57 | (WinForms and WPF projects will have this by default). If you use the Task Dialog from a 58 | different thread than the Main Thread, you will need to set it to 59 | [ApartmentState.STA](https://docs.microsoft.com/dotnet/api/system.threading.apartmentstate). 60 | 61 | 62 | ## Using the Task Dialog 63 | 64 | Show a simple dialog: 65 | ```c# 66 | TaskDialogResult result = TaskDialog.Show( 67 | text: "This is a new dialog!", 68 | instruction: "Hi there!", 69 | buttons: TaskDialogButtons.Yes | TaskDialogButtons.No, 70 | icon: TaskDialogIcon.Information); 71 | ``` 72 | 73 | Show a dialog with command links and a marquee progress bar: 74 | ```c# 75 | TaskDialogContents contents = new TaskDialogContents() 76 | { 77 | Instruction = "Hi there!", 78 | Text = "This is a new dialog!", 79 | Icon = TaskDialogIcon.Information, 80 | CommandLinkMode = TaskDialogCommandLinkMode.CommandLinks, // Show command links instead of custom buttons 81 | 82 | ProgressBar = new TaskDialogProgressBar() 83 | { 84 | State = TaskDialogProgressBarState.Marquee 85 | } 86 | }; 87 | 88 | // Create a command link and a "Cancel" common button. 89 | // Note: Adding a "Cancel" button will automatically show a "X" button in 90 | // the dialog's title bar, and the user can press ESC to cancel the dialog. 91 | TaskDialogCustomButton customButton = contents.CustomButtons.Add("My Command Link"); 92 | TaskDialogCommonButton buttonCancel = contents.CommonButtons.Add(TaskDialogResult.Cancel); 93 | 94 | // Show the dialog and check which button the user has clicked. 95 | using (TaskDialog dialog = new TaskDialog(contents)) 96 | { 97 | TaskDialogButton result = dialog.Show(); 98 | } 99 | ``` 100 | 101 | Update the dialog's content when clicking one of its buttons: 102 | 103 | ```c# 104 | int number = 0; 105 | 106 | TaskDialogContents contents = new TaskDialogContents() 107 | { 108 | Instruction = "Update number?", 109 | Text = $"Current number: {number}", 110 | Icon = (TaskDialogIcon)99 111 | }; 112 | 113 | var buttonYes = contents.CommonButtons.Add(TaskDialogResult.Yes); 114 | var buttonClose = contents.CommonButtons.Add(TaskDialogResult.Close); 115 | 116 | // Handle the event when the "Yes" button was clicked. 117 | buttonYes.Click += (s, e) => 118 | { 119 | // When clicking the "Yes" button, don't close the dialog, but 120 | // instead increment the number and update the dialog content. 121 | e.CancelClose = true; 122 | 123 | // Update the content. 124 | number++; 125 | contents.Text = $"Current number: {number}"; 126 | }; 127 | 128 | using (TaskDialog dialog = new TaskDialog(contents)) 129 | { 130 | dialog.Show(); 131 | } 132 | ``` 133 | 134 | For a more detailed example of a TaskDialog that uses progress bars, a timer, 135 | hyperlinks, navigation and various event handlers (as shown by the screenshots), please 136 | see the [`TaskDialog.Example`](/TaskDialog.Example/Program.cs) project. 137 | 138 | 139 | ### Non-modal dialog 140 | 141 | Be aware that when you show a non-modal Task Dialog by specifying `null` or `IntPtr.Zero` as 142 | owner window, the `TaskDialog.Show()` method will still not return until the dialog is closed; 143 | in contrast to other implementations like `Form.Show()` (WinForms) where `Show()` 144 | displays the window and then returns immediately. 145 | 146 | This means that when you simultaneously show multiple non-modal Task Dialogs, the `Show()` 147 | method will occur multiple times in the call stack (as each will run the event loop), and 148 | therefore when you close an older dialog, its corresponding `Show()` method cannot return 149 | until all other (newer) Task Dialogs are also closed. However, the corresponding 150 | `TaskDialog.CommonButtonClicked` and `ITaskDialogCustomButton.ButtonClicked` events will 151 | be called just before the dialog is closed. 152 | 153 | E.g. if you repeatedly open a new dialog and then close a previously opened one, the 154 | call stack will fill with more and more `Show()` calls until all the dialogs are closed. 155 | Note that in that case, the `TimerTick` event will also continue to be called for the 156 | already closed dialogs until their `Show()` method can return. 157 | 158 | 159 | ## Internal details/notes 160 | 161 | For the Task Dialog callback, a static delegate is used to avoid the overhead of creating 162 | native functions during runtime for each new Task Dialog instance. A 163 | [`GCHandle`](https://docs.microsoft.com/dotnet/api/system.runtime.interopservices.gchandle) 164 | is used in the callback to map the supplied reference data back to the actual Task Dialog 165 | instance. 166 | -------------------------------------------------------------------------------- /TaskDialog.Example/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Windows.Forms; 3 | using KPreisser.UI; 4 | 5 | namespace TaskDialogExample 6 | { 7 | class Program 8 | { 9 | [STAThread] 10 | static void Main() 11 | { 12 | ShowTaskDialogExample(); 13 | 14 | Console.ReadKey(); 15 | } 16 | 17 | private static void ShowTaskDialogExample() 18 | { 19 | var dialogPage = new TaskDialogPage() 20 | { 21 | Title = "Example 1", 22 | Instruction = "Hello Task Dialog! 👍", 23 | Text = "Hi, this is the Content.\nBlah blah blah…", 24 | Icon = TaskDialogStandardIcon.SecuritySuccessGreenBar, 25 | 26 | Footer = 27 | { 28 | Text = "This is the footer.", 29 | Icon = TaskDialogStandardIcon.Warning, 30 | }, 31 | 32 | Expander = 33 | { 34 | Text = "Expanded Information!", 35 | ExpandFooterArea = true 36 | }, 37 | 38 | ProgressBar = new TaskDialogProgressBar(), 39 | 40 | CustomButtonStyle = TaskDialogCustomButtonStyle.CommandLinks, 41 | EnableHyperlinks = true, 42 | AllowCancel = true, 43 | CanBeMinimized = true, 44 | SizeToContent = true, 45 | }; 46 | dialogPage.Created += (s, e) => 47 | { 48 | Console.WriteLine("Main Contents created!"); 49 | }; 50 | dialogPage.Destroyed += (s, e) => 51 | { 52 | Console.WriteLine("Main Contents destroyed!"); 53 | }; 54 | 55 | dialogPage.Expander.ExpandedChanged += (s, e) => 56 | { 57 | Console.WriteLine("Expander Expanded Changed: " + dialogPage.Expander.Expanded); 58 | }; 59 | 60 | var dialog = new TaskDialog(dialogPage); 61 | dialog.Opened += (s, e) => 62 | { 63 | Console.WriteLine("Dialog opened!"); 64 | }; 65 | dialog.Shown += (s, e) => 66 | { 67 | Console.WriteLine("Dialog shown!"); 68 | }; 69 | dialog.Closing += (s, e) => 70 | { 71 | Console.WriteLine("Dialog closing!"); 72 | }; 73 | dialog.Closed += (s, e) => 74 | { 75 | Console.WriteLine("Dialog closed!"); 76 | }; 77 | //dialog.Activated += (s, e) => 78 | //{ 79 | // Console.WriteLine("Dialog activated!"); 80 | //}; 81 | //dialog.Deactivated += (s, e) => 82 | //{ 83 | // Console.WriteLine("Dialog deactivated!"); 84 | //}; 85 | 86 | dialogPage.ProgressBar.Value = 1; 87 | 88 | TaskDialogStandardButton buttonYes = dialogPage.StandardButtons.Add(TaskDialogResult.Yes); 89 | buttonYes.Enabled = false; 90 | TaskDialogStandardButton buttonNo = dialogPage.StandardButtons.Add(TaskDialogResult.No); 91 | 92 | // Add a hidden "Cancel" button so that we can get notified when the user 93 | // closes the dialog through the window's X button or with ESC (and could 94 | // cancel the close operation). 95 | TaskDialogStandardButton buttonCancelHidden = dialogPage.StandardButtons.Add(TaskDialogResult.Cancel); 96 | buttonCancelHidden.Visible = false; 97 | buttonCancelHidden.Click += (s, e) => 98 | { 99 | Console.WriteLine("Cancel clicked!"); 100 | }; 101 | 102 | long timerCount = 2; 103 | var dialogPageTimer = null as Timer; 104 | dialogPage.Created += (s, e) => 105 | { 106 | dialogPageTimer = new Timer() 107 | { 108 | Enabled = true, 109 | Interval = 200 110 | }; 111 | dialogPageTimer.Tick += (s2, e2) => 112 | { 113 | // Update the progress bar if value <= 35. 114 | if (timerCount <= 35) 115 | { 116 | dialogPage.ProgressBar.Value = (int)timerCount; 117 | } 118 | else if (timerCount == 36) 119 | { 120 | dialogPage.ProgressBar.State = TaskDialogProgressBarState.Paused; 121 | } 122 | 123 | timerCount++; 124 | }; 125 | }; 126 | dialogPage.Destroyed += (s, e) => 127 | { 128 | dialogPageTimer.Dispose(); 129 | dialogPageTimer = null; 130 | }; 131 | 132 | dialogPage.HyperlinkClicked += (s, e) => 133 | { 134 | Console.WriteLine("Hyperlink clicked!"); 135 | TaskDialog.Show(dialog, "Clicked Hyperlink: " + e.Hyperlink, icon: TaskDialogStandardIcon.Information); 136 | }; 137 | 138 | // Create custom buttons that are shown as command links. 139 | TaskDialogCustomButton button1 = dialogPage.CustomButtons.Add("Change Icon + Enable Buttons ✔"); 140 | TaskDialogCustomButton button2 = dialogPage.CustomButtons.Add("Disabled Button 🎵🎶", "After enabling, can show a new dialog."); 141 | TaskDialogCustomButton button3 = dialogPage.CustomButtons.Add("Some Admin Action…", "Navigates to a new dialog page."); 142 | button3.ElevationRequired = true; 143 | 144 | TaskDialogStandardIcon nextIcon = TaskDialogStandardIcon.SecuritySuccessGreenBar; 145 | button1.Click += (s, e) => 146 | { 147 | Console.WriteLine("Button1 clicked!"); 148 | 149 | // Don't close the dialog. 150 | e.CancelClose = true; 151 | 152 | nextIcon++; 153 | 154 | // Set the icon and the content. 155 | dialogPage.Icon = nextIcon; 156 | dialogPage.Instruction = "Icon: " + nextIcon; 157 | 158 | // Enable the "Yes" button and the 3rd button when the checkbox is set. 159 | buttonYes.Enabled = true; 160 | button2.Enabled = true; 161 | }; 162 | 163 | button2.Enabled = false; 164 | button2.Click += (s, e) => 165 | { 166 | Console.WriteLine("Button2 clicked!"); 167 | 168 | // Don't close the main dialog. 169 | e.CancelClose = true; 170 | 171 | // Show a new Taskdialog that shows an incrementing number. 172 | var newPage = new TaskDialogPage() 173 | { 174 | Text = "This is a new non-modal dialog!", 175 | Icon = TaskDialogStandardIcon.Information, 176 | }; 177 | 178 | TaskDialogStandardButton buttonClose = newPage.StandardButtons.Add(TaskDialogResult.Close); 179 | TaskDialogStandardButton buttonContinue = newPage.StandardButtons.Add(TaskDialogResult.Continue); 180 | 181 | int number = 0; 182 | void UpdateNumberText(bool callUpdate = true) 183 | { 184 | // Update the instruction with the new number. 185 | newPage.Instruction = "Hi there! Number: " + number.ToString(); 186 | } 187 | UpdateNumberText(false); 188 | 189 | var newPageTimer = null as Timer; 190 | newPage.Created += (s2, e2) => 191 | { 192 | newPageTimer = new Timer() 193 | { 194 | Enabled = true, 195 | Interval = 200 196 | }; 197 | newPageTimer.Tick += (s3, e3) => 198 | { 199 | number++; 200 | UpdateNumberText(); 201 | }; 202 | }; 203 | newPage.Destroyed += (s2, e2) => 204 | { 205 | newPageTimer.Dispose(); 206 | newPageTimer = null; 207 | }; 208 | 209 | buttonContinue.Click += (s2, e2) => 210 | { 211 | Console.WriteLine("New dialog - Continue Button clicked"); 212 | 213 | e2.CancelClose = true; 214 | number += 1000; 215 | UpdateNumberText(); 216 | }; 217 | 218 | var innerDialog = new TaskDialog(newPage); 219 | TaskDialogButton innerResult = innerDialog.Show(); 220 | Console.WriteLine("Result of new dialog: " + innerResult); 221 | }; 222 | 223 | button3.Click += (s, e) => 224 | { 225 | Console.WriteLine("Button3 clicked!"); 226 | 227 | // Don't close the dialog from the button click. 228 | e.CancelClose = true; 229 | 230 | // Create a new contents instance to which we will navigate the dialog. 231 | var newContents = new TaskDialogPage() 232 | { 233 | Instruction = "Page 2", 234 | Text = "Welcome to the second page!", 235 | Icon = TaskDialogStandardIcon.SecurityShieldBlueBar, 236 | SizeToContent = true, 237 | 238 | CheckBox = 239 | { 240 | Text = "I think I agree…" 241 | }, 242 | ProgressBar = 243 | { 244 | State = TaskDialogProgressBarState.Marquee 245 | } 246 | }; 247 | newContents.Created += (s2, e2) => 248 | { 249 | Console.WriteLine("New Contents created!"); 250 | 251 | // Set a new icon after navigating the dialog. This allows us to show the 252 | // yellow bar from the "SecurityWarningBar" icon with a different icon. 253 | newContents.Icon = TaskDialogStandardIcon.Warning; 254 | }; 255 | newContents.Destroyed += (s2, e2) => 256 | { 257 | Console.WriteLine("New Contents destroyed!"); 258 | }; 259 | 260 | TaskDialogStandardButton buttonCancel = newContents.StandardButtons.Add(TaskDialogResult.Cancel); 261 | buttonCancel.Enabled = false; 262 | buttonCancel.ElevationRequired = true; 263 | 264 | // Create a custom button that will be shown as regular button. 265 | TaskDialogCustomButton customButton = newContents.CustomButtons.Add("My Button :)"); 266 | 267 | // Add radio buttons. 268 | TaskDialogRadioButton radioButton1 = newContents.RadioButtons.Add("My Radio Button 1"); 269 | TaskDialogRadioButton radioButton2 = newContents.RadioButtons.Add("My Radio Button 2"); 270 | radioButton2.Checked = true; 271 | 272 | radioButton1.CheckedChanged += (s2, e2) => Console.WriteLine( 273 | $"Radio Button 1 CheckedChanged: RB1={radioButton1.Checked}, RB2={radioButton2.Checked}"); 274 | radioButton2.CheckedChanged += (s2, e2) => Console.WriteLine( 275 | $"Radio Button 2 CheckedChanged: RB1={radioButton1.Checked}, RB2={radioButton2.Checked}"); 276 | 277 | newContents.CheckBox.CheckedChanged += (s2, e2) => 278 | { 279 | Console.WriteLine("Checkbox CheckedChanged: " + newContents.CheckBox.Checked); 280 | 281 | buttonCancel.Enabled = newContents.CheckBox.Checked; 282 | }; 283 | 284 | // Now navigate the dialog. 285 | dialog.Page = newContents; 286 | }; 287 | 288 | TaskDialogButton result = dialog.Show(); 289 | 290 | Console.WriteLine("Result of main dialog: " + result); 291 | } 292 | } 293 | } 294 | 295 | -------------------------------------------------------------------------------- /TaskDialog.Example/TaskDialog.Example.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net472 6 | app.manifest 7 | 7.1 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /TaskDialog.Example/app.manifest: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 52 | 53 | 54 | true 55 | 56 | 57 | 58 | 59 | 60 | 61 | 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /TaskDialog.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.28010.2050 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TaskDialog", "TaskDialog\TaskDialog.csproj", "{38F2BA6E-EC02-4D24-8467-834A099EDF4A}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TaskDialog.Example", "TaskDialog.Example\TaskDialog.Example.csproj", "{1C6E0A8F-93F3-4233-88F0-00F05A7AA34E}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {38F2BA6E-EC02-4D24-8467-834A099EDF4A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {38F2BA6E-EC02-4D24-8467-834A099EDF4A}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {38F2BA6E-EC02-4D24-8467-834A099EDF4A}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {38F2BA6E-EC02-4D24-8467-834A099EDF4A}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {1C6E0A8F-93F3-4233-88F0-00F05A7AA34E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {1C6E0A8F-93F3-4233-88F0-00F05A7AA34E}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {1C6E0A8F-93F3-4233-88F0-00F05A7AA34E}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {1C6E0A8F-93F3-4233-88F0-00F05A7AA34E}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {CF01D255-7493-42FE-8EFE-9E11D68BF2EE} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /TaskDialog/TaskDialog.WindowSubclassHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace KPreisser.UI 4 | { 5 | public partial class TaskDialog 6 | { 7 | private class WindowSubclassHandler : UI.WindowSubclassHandler 8 | { 9 | private readonly TaskDialog _taskDialog; 10 | 11 | private bool _processedShowWindowMessage; 12 | 13 | public WindowSubclassHandler(TaskDialog taskDialog) 14 | : base(taskDialog?._hwndDialog ?? throw new ArgumentNullException(nameof(taskDialog))) 15 | { 16 | _taskDialog = taskDialog; 17 | } 18 | 19 | protected override unsafe IntPtr WndProc(int msg, IntPtr wParam, IntPtr lParam) 20 | { 21 | switch (msg) 22 | { 23 | case TaskDialogNativeMethods.WM_WINDOWPOSCHANGED: 24 | IntPtr result = base.WndProc(msg, wParam, lParam); 25 | 26 | ref TaskDialogNativeMethods.WINDOWPOS windowPos = 27 | ref *(TaskDialogNativeMethods.WINDOWPOS*)lParam; 28 | 29 | if ((windowPos.flags & TaskDialogNativeMethods.WINDOWPOS_FLAGS.SWP_SHOWWINDOW) == 30 | TaskDialogNativeMethods.WINDOWPOS_FLAGS.SWP_SHOWWINDOW && 31 | !_processedShowWindowMessage) 32 | { 33 | _processedShowWindowMessage = true; 34 | 35 | // The task dialog window has been shown for the first time. 36 | _taskDialog.OnShown(EventArgs.Empty); 37 | } 38 | 39 | return result; 40 | 41 | case ContinueButtonClickHandlingMessage: 42 | // We received the message which we posted earlier when 43 | // handling a TDN_BUTTON_CLICKED notification, so we should 44 | // no longer ignore such notifications. 45 | _taskDialog._ignoreButtonClickedNotifications = false; 46 | 47 | // Do not forward the message to the base class. 48 | return IntPtr.Zero; 49 | 50 | default: 51 | return base.WndProc(msg, wParam, lParam); 52 | } 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /TaskDialog/TaskDialog.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net472;netstandard2.0 5 | TaskDialog 6 | en-US 7 | TaskDialog 8 | KPreisser.UI 9 | 7.3 10 | True 11 | 12 | 13 | 14 | True 15 | full 16 | bin\Debug\ 17 | TRACE;DEBUG 18 | $(DefineConstants);NET_STANDARD 19 | bin\Debug\$(TargetFramework)\TaskDialog.xml 20 | True 21 | 22 | True 23 | 24 | 25 | 26 | True 27 | pdbonly 28 | bin\Release\ 29 | TRACE 30 | $(DefineConstants);NET_STANDARD 31 | bin\Release\$(TargetFramework)\TaskDialog.xml 32 | True 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /TaskDialog/TaskDialogButton.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace KPreisser.UI 5 | { 6 | /// 7 | /// 8 | /// 9 | public abstract class TaskDialogButton : TaskDialogControl 10 | { 11 | private bool _enabled = true; 12 | 13 | private bool _defaultButton; 14 | 15 | private bool _elevationRequired; 16 | 17 | private IReadOnlyList _collection; 18 | 19 | /// 20 | /// Occurs when the button is clicked. 21 | /// 22 | /// 23 | /// By default, the dialog will be closed after the event handler returns 24 | /// (except for the button which instead 25 | /// will raise the event afterwards). 26 | /// To prevent the dialog from closing, set the 27 | /// property to 28 | /// true. 29 | /// 30 | public event EventHandler Click; 31 | 32 | // Disallow inheritance by specifying a private protected constructor. 33 | private protected TaskDialogButton() 34 | : base() 35 | { 36 | } 37 | 38 | /// 39 | /// 40 | /// 41 | /// 42 | /// This property can be set while the dialog is shown. 43 | /// 44 | public bool Enabled 45 | { 46 | get => _enabled; 47 | 48 | set 49 | { 50 | DenyIfBoundAndNotCreated(); 51 | 52 | // Check if we can update the button. 53 | if (CanUpdate()) 54 | { 55 | BoundPage.BoundTaskDialog.SetButtonEnabled( 56 | ButtonID, 57 | value); 58 | } 59 | 60 | _enabled = value; 61 | } 62 | } 63 | 64 | /// 65 | /// 66 | /// 67 | /// 68 | /// This property can be set while the dialog is shown. 69 | /// 70 | public bool ElevationRequired 71 | { 72 | get => _elevationRequired; 73 | 74 | set 75 | { 76 | DenyIfBoundAndNotCreated(); 77 | 78 | if (CanUpdate()) 79 | { 80 | BoundPage.BoundTaskDialog.SetButtonElevationRequiredState( 81 | ButtonID, 82 | value); 83 | } 84 | 85 | _elevationRequired = value; 86 | } 87 | } 88 | 89 | /// 90 | /// Gets or sets a value that indicates if this button will be the default button 91 | /// in the Task Dialog. 92 | /// 93 | public bool DefaultButton 94 | { 95 | get => _defaultButton; 96 | 97 | set 98 | { 99 | _defaultButton = value; 100 | 101 | // If we are part of a collection, set the defaultButton value of 102 | // all other buttons to false. 103 | // Note that this does not handle buttons that are added later to 104 | // the collection. 105 | if (_collection != null && value) 106 | { 107 | foreach (TaskDialogButton button in _collection) 108 | button._defaultButton = button == this; 109 | } 110 | } 111 | } 112 | 113 | internal abstract int ButtonID 114 | { 115 | get; 116 | } 117 | 118 | // Note: Instead of declaring an abstract Collection getter, we implement 119 | // the field and the property here so that the subclass doesn't have to 120 | // do the implementation, in order to avoid duplicating the logic 121 | // (e.g. if we ever need to add actions in the setter, it normally would 122 | // be the same for all subclasses). Instead, the subclass can declare 123 | // a new (internal) Collection property which has a more specific type. 124 | private protected IReadOnlyList Collection 125 | { 126 | get => _collection; 127 | set => _collection = value; 128 | } 129 | 130 | /// 131 | /// Simulates a click on this button. 132 | /// 133 | public void PerformClick() 134 | { 135 | // Note: We allow a click even if the button is not visible/created. 136 | DenyIfNotBoundOrWaitingForInitialization(); 137 | 138 | BoundPage.BoundTaskDialog.ClickButton(ButtonID); 139 | } 140 | 141 | internal bool HandleButtonClicked() 142 | { 143 | var e = new TaskDialogButtonClickedEventArgs(); 144 | OnClick(e); 145 | 146 | return !e.CancelClose; 147 | } 148 | 149 | private protected override void ApplyInitializationCore() 150 | { 151 | // Re-set the properties so they will make the necessary calls. 152 | if (!_enabled) 153 | Enabled = _enabled; 154 | if (_elevationRequired) 155 | ElevationRequired = _elevationRequired; 156 | } 157 | 158 | private protected void OnClick(TaskDialogButtonClickedEventArgs e) 159 | { 160 | Click?.Invoke(this, e); 161 | } 162 | 163 | private bool CanUpdate() 164 | { 165 | // Only update the button when bound to a task dialog and we are not 166 | // waiting for the Navigated event. In the latter case we don't throw 167 | // an exception however, because ApplyInitialization() will be called 168 | // in the Navigated handler that does the necessary updates. 169 | return BoundPage?.WaitingForInitialization == false; 170 | } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /TaskDialog/TaskDialogButtonClickedEventArgs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace KPreisser.UI 4 | { 5 | /// 6 | /// 7 | /// 8 | public class TaskDialogButtonClickedEventArgs : EventArgs 9 | { 10 | /// 11 | /// 12 | /// 13 | internal TaskDialogButtonClickedEventArgs() 14 | : base() 15 | { 16 | } 17 | 18 | /// 19 | /// Gets or sets a value that indicates if the dialog should not be closed 20 | /// after the event handler returns. 21 | /// 22 | /// 23 | /// When you don't set this property to true, the 24 | /// event will occur afterwards. 25 | /// 26 | public bool CancelClose 27 | { 28 | get; 29 | set; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /TaskDialog/TaskDialogButtons.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace KPreisser.UI 4 | { 5 | /// 6 | /// 7 | /// 8 | [Flags] 9 | public enum TaskDialogButtons : int 10 | { 11 | /// 12 | /// 13 | /// 14 | None = 0, 15 | 16 | /// 17 | /// 18 | /// 19 | OK = 1 << 0, 20 | 21 | /// 22 | /// 23 | /// 24 | Yes = 1 << 1, 25 | 26 | /// 27 | /// 28 | /// 29 | No = 1 << 2, 30 | 31 | /// 32 | /// 33 | /// 34 | /// 35 | /// Note: Adding a Cancel button will automatically add a close button 36 | /// to the task dialog's title bar and will allow to close the dialog by 37 | /// pressing ESC or Alt+F4 (just as if you enabled 38 | /// ). 39 | /// 40 | Cancel = 1 << 3, 41 | 42 | /// 43 | /// 44 | /// 45 | Retry = 1 << 4, 46 | 47 | /// 48 | /// 49 | /// 50 | Close = 1 << 5, 51 | 52 | /// 53 | /// 54 | /// 55 | Abort = 1 << 16, 56 | 57 | /// 58 | /// 59 | /// 60 | Ignore = 1 << 17, 61 | 62 | /// 63 | /// 64 | /// 65 | TryAgain = 1 << 18, 66 | 67 | /// 68 | /// 69 | /// 70 | Continue = 1 << 19, 71 | 72 | /// 73 | /// 74 | /// 75 | /// 76 | /// Note: Clicking this button will not close the dialog, but will raise the 77 | /// event. 78 | /// 79 | Help = 1 << 20 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /TaskDialog/TaskDialogCheckbox.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using TaskDialogFlags = KPreisser.UI.TaskDialogNativeMethods.TASKDIALOG_FLAGS; 4 | 5 | namespace KPreisser.UI 6 | { 7 | /// 8 | /// 9 | /// 10 | public sealed class TaskDialogCheckBox : TaskDialogControl 11 | { 12 | private string _text; 13 | 14 | private bool _checked; 15 | 16 | /// 17 | /// 18 | /// 19 | public event EventHandler CheckedChanged; 20 | 21 | /// 22 | /// 23 | /// 24 | public TaskDialogCheckBox() 25 | : base() 26 | { 27 | } 28 | 29 | /// 30 | /// 31 | /// 32 | /// 33 | public TaskDialogCheckBox(string text) 34 | : this() 35 | { 36 | _text = text; 37 | } 38 | 39 | /// 40 | /// 41 | /// 42 | public string Text 43 | { 44 | get => _text; 45 | 46 | set 47 | { 48 | DenyIfBound(); 49 | 50 | _text = value; 51 | } 52 | } 53 | 54 | /// 55 | /// 56 | /// 57 | /// 58 | /// This property can be set while the dialog is shown. 59 | /// 60 | public bool Checked 61 | { 62 | get => _checked; 63 | 64 | set 65 | { 66 | DenyIfBoundAndNotCreated(); 67 | DenyIfWaitingForInitialization(); 68 | 69 | if (BoundPage == null) 70 | { 71 | _checked = value; 72 | } 73 | else 74 | { 75 | // Click the checkbox which should cause a call to 76 | // HandleCheckBoxClicked(), where we will update the checked 77 | // state. 78 | BoundPage.BoundTaskDialog.ClickCheckBox( 79 | value); 80 | } 81 | } 82 | } 83 | 84 | internal override bool IsCreatable 85 | { 86 | get => base.IsCreatable && !TaskDialogPage.IsNativeStringNullOrEmpty(_text); 87 | } 88 | 89 | /// 90 | /// 91 | /// 92 | public void Focus() 93 | { 94 | DenyIfNotBoundOrWaitingForInitialization(); 95 | DenyIfBoundAndNotCreated(); 96 | 97 | BoundPage.BoundTaskDialog.ClickCheckBox( 98 | _checked, 99 | true); 100 | } 101 | 102 | /// 103 | /// 104 | /// 105 | /// 106 | public override string ToString() 107 | { 108 | return _text ?? base.ToString(); 109 | } 110 | 111 | internal void HandleCheckBoxClicked(bool @checked) 112 | { 113 | // Only raise the event if the state actually changed. 114 | if (@checked != _checked) 115 | { 116 | _checked = @checked; 117 | OnCheckedChanged(EventArgs.Empty); 118 | } 119 | } 120 | 121 | private protected override TaskDialogFlags BindCore() 122 | { 123 | TaskDialogFlags flags = base.BindCore(); 124 | 125 | if (_checked) 126 | flags |= TaskDialogFlags.TDF_VERIFICATION_FLAG_CHECKED; 127 | 128 | return flags; 129 | } 130 | 131 | /// 132 | /// 133 | /// 134 | /// 135 | private void OnCheckedChanged(EventArgs e) 136 | { 137 | CheckedChanged?.Invoke(this, e); 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /TaskDialog/TaskDialogClosingEventArgs.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | 3 | namespace KPreisser.UI 4 | { 5 | /// 6 | /// 7 | /// 8 | public class TaskDialogClosingEventArgs : CancelEventArgs 9 | { 10 | /// 11 | /// 12 | /// 13 | internal TaskDialogClosingEventArgs(TaskDialogButton closeButton) 14 | : base() 15 | { 16 | CloseButton = closeButton; 17 | } 18 | 19 | /// 20 | /// Gets the that is causing the task dialog 21 | /// to close. 22 | /// 23 | public TaskDialogButton CloseButton 24 | { 25 | get; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /TaskDialog/TaskDialogControl.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using TaskDialogFlags = KPreisser.UI.TaskDialogNativeMethods.TASKDIALOG_FLAGS; 4 | 5 | namespace KPreisser.UI 6 | { 7 | /// 8 | /// 9 | /// 10 | public abstract class TaskDialogControl 11 | { 12 | // Disallow inheritance by specifying a private protected constructor. 13 | private protected TaskDialogControl() 14 | : base() 15 | { 16 | } 17 | 18 | /// 19 | /// Gets or sets the object that contains data about the control. 20 | /// 21 | public object Tag 22 | { 23 | get; 24 | set; 25 | } 26 | 27 | internal TaskDialogPage BoundPage 28 | { 29 | get; 30 | private set; 31 | } 32 | 33 | /// 34 | /// Gets a value that indicates if the current state of this control 35 | /// allows it to be created in a task dialog when binding it. 36 | /// 37 | internal virtual bool IsCreatable 38 | { 39 | get => true; 40 | } 41 | 42 | /// 43 | /// Gets or sets a value that indicates if this control has been created 44 | /// in a bound task dialog. 45 | /// 46 | internal bool IsCreated 47 | { 48 | get; 49 | private set; 50 | } 51 | 52 | internal TaskDialogFlags Bind(TaskDialogPage page) 53 | { 54 | BoundPage = page ?? 55 | throw new ArgumentNullException(nameof(page)); 56 | 57 | // Use the current value of IsCreatable to determine if the control is 58 | // created. This is important because IsCreatable can change while the 59 | // control is displayed (e.g. if it depends on the Text property). 60 | IsCreated = IsCreatable; 61 | 62 | return IsCreated ? BindCore() : default; 63 | } 64 | 65 | internal void Unbind() 66 | { 67 | if (IsCreated) 68 | UnbindCore(); 69 | 70 | IsCreated = false; 71 | BoundPage = null; 72 | } 73 | 74 | /// 75 | /// Applies initialization after the task dialog is displayed or navigated. 76 | /// 77 | internal void ApplyInitialization() 78 | { 79 | // Only apply the initialization if the control is actually created. 80 | if (IsCreated) 81 | ApplyInitializationCore(); 82 | } 83 | 84 | /// 85 | /// When overridden in a subclass, runs additional binding logic and returns 86 | /// flags to be specified before the task dialog is displayed or navigated. 87 | /// 88 | /// 89 | /// This method will only be called if returns true. 90 | /// 91 | /// 92 | private protected virtual TaskDialogFlags BindCore() 93 | { 94 | return default; 95 | } 96 | 97 | /// 98 | /// 99 | /// 100 | /// 101 | /// This method will only be called if was called. 102 | /// 103 | private protected virtual void UnbindCore() 104 | { 105 | } 106 | 107 | /// 108 | /// When overridden in a subclass, applies initialization after the task dialog 109 | /// is displayed or navigated. 110 | /// 111 | /// 112 | /// This method will only be called if returns true. 113 | /// 114 | private protected virtual void ApplyInitializationCore() 115 | { 116 | } 117 | 118 | private protected void DenyIfBound() 119 | { 120 | BoundPage?.DenyIfBound(); 121 | } 122 | 123 | private protected void DenyIfWaitingForInitialization() 124 | { 125 | BoundPage?.DenyIfWaitingForInitialization(); 126 | } 127 | 128 | private protected void DenyIfNotBoundOrWaitingForInitialization() 129 | { 130 | DenyIfWaitingForInitialization(); 131 | 132 | if (BoundPage == null) 133 | throw new InvalidOperationException( 134 | "This control is not currently bound to a task dialog."); 135 | } 136 | 137 | private protected void DenyIfBoundAndNotCreated() 138 | { 139 | if (BoundPage != null && !IsCreated) 140 | throw new InvalidOperationException("The control has not been created."); 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /TaskDialog/TaskDialogCustomButton.cs: -------------------------------------------------------------------------------- 1 | using TaskDialogFlags = KPreisser.UI.TaskDialogNativeMethods.TASKDIALOG_FLAGS; 2 | 3 | namespace KPreisser.UI 4 | { 5 | /// 6 | /// 7 | /// 8 | public sealed class TaskDialogCustomButton : TaskDialogButton 9 | { 10 | private string _text; 11 | 12 | private string _descriptionText; 13 | 14 | private int _buttonID; 15 | 16 | /// 17 | /// 18 | /// 19 | public TaskDialogCustomButton() 20 | : base() 21 | { 22 | } 23 | 24 | /// 25 | /// 26 | /// 27 | public TaskDialogCustomButton(string text, string descriptionText = null) 28 | : this() 29 | { 30 | _text = text; 31 | _descriptionText = descriptionText; 32 | } 33 | 34 | /// 35 | /// 36 | /// 37 | public string Text 38 | { 39 | get => _text; 40 | 41 | set 42 | { 43 | DenyIfBound(); 44 | 45 | _text = value; 46 | } 47 | } 48 | 49 | /// 50 | /// Gets or sets an additional description text that will be displayed in 51 | /// a separate line of the command link when 52 | /// is set to 53 | /// or 54 | /// . 55 | /// 56 | public string DescriptionText 57 | { 58 | get => _descriptionText; 59 | 60 | set 61 | { 62 | DenyIfBound(); 63 | 64 | _descriptionText = value; 65 | } 66 | } 67 | 68 | internal override bool IsCreatable 69 | { 70 | get => base.IsCreatable && !TaskDialogPage.IsNativeStringNullOrEmpty(_text); 71 | } 72 | 73 | internal override int ButtonID 74 | { 75 | get => _buttonID; 76 | } 77 | 78 | internal new TaskDialogCustomButtonCollection Collection 79 | { 80 | get => (TaskDialogCustomButtonCollection)base.Collection; 81 | set => base.Collection = value; 82 | } 83 | 84 | /// 85 | /// 86 | /// 87 | /// 88 | public override string ToString() 89 | { 90 | return _text ?? base.ToString(); 91 | } 92 | 93 | internal TaskDialogFlags Bind(TaskDialogPage page, int buttonID) 94 | { 95 | TaskDialogFlags result = Bind(page); 96 | _buttonID = buttonID; 97 | 98 | return result; 99 | } 100 | 101 | internal string GetResultingText() 102 | { 103 | TaskDialogPage page = BoundPage; 104 | 105 | // Remove LFs from the text. Otherwise, the dialog would display the 106 | // part of the text after the LF in the command link note, but for 107 | // this we have the "DescriptionText" property, so we should ensure that 108 | // there is not an discrepancy here and that the contents of the "Text" 109 | // property are not displayed in the command link note. 110 | // Therefore, we replace a combined CR+LF with CR, and then also single 111 | // LFs with CR, because CR is treated as a line break. 112 | string text = _text?.Replace("\r\n", "\r").Replace("\n", "\r"); 113 | 114 | if ((page?.CustomButtonStyle == TaskDialogCustomButtonStyle.CommandLinks || 115 | page?.CustomButtonStyle == TaskDialogCustomButtonStyle.CommandLinksNoIcon) && 116 | text != null && _descriptionText != null) 117 | text += '\n' + _descriptionText; 118 | 119 | return text; 120 | } 121 | 122 | private protected override void UnbindCore() 123 | { 124 | _buttonID = 0; 125 | 126 | base.UnbindCore(); 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /TaskDialog/TaskDialogCustomButtonCollection.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.ObjectModel; 4 | 5 | namespace KPreisser.UI 6 | { 7 | /// 8 | /// 9 | /// 10 | public class TaskDialogCustomButtonCollection 11 | : Collection 12 | { 13 | // HashSet to detect duplicate items. 14 | private readonly HashSet _itemSet = 15 | new HashSet(); 16 | 17 | private TaskDialogPage _boundPage; 18 | 19 | /// 20 | /// 21 | /// 22 | public TaskDialogCustomButtonCollection() 23 | : base() 24 | { 25 | } 26 | 27 | internal TaskDialogPage BoundPage 28 | { 29 | get => _boundPage; 30 | set => _boundPage = value; 31 | } 32 | 33 | /// 34 | /// 35 | /// 36 | /// 37 | /// 38 | /// 39 | public TaskDialogCustomButton Add(string text, string descriptionText = null) 40 | { 41 | var button = new TaskDialogCustomButton() 42 | { 43 | Text = text, 44 | DescriptionText = descriptionText 45 | }; 46 | 47 | Add(button); 48 | return button; 49 | } 50 | 51 | /// 52 | /// 53 | /// 54 | /// 55 | /// 56 | protected override void SetItem(int index, TaskDialogCustomButton item) 57 | { 58 | // Disallow collection modification, so that we don't need to copy it 59 | // when binding the TaskDialogPage. 60 | _boundPage?.DenyIfBound(); 61 | DenyIfHasOtherCollection(item); 62 | 63 | TaskDialogCustomButton oldItem = this[index]; 64 | if (oldItem != item) 65 | { 66 | // First, add the new item (which will throw if it is a duplicate entry), 67 | // then remove the old one. 68 | if (!_itemSet.Add(item)) 69 | throw new ArgumentException(); 70 | _itemSet.Remove(oldItem); 71 | 72 | oldItem.Collection = null; 73 | item.Collection = this; 74 | } 75 | 76 | base.SetItem(index, item); 77 | } 78 | 79 | /// 80 | /// 81 | /// 82 | /// 83 | /// 84 | protected override void InsertItem(int index, TaskDialogCustomButton item) 85 | { 86 | // Disallow collection modification, so that we don't need to copy it 87 | // when binding the TaskDialogPage. 88 | _boundPage?.DenyIfBound(); 89 | DenyIfHasOtherCollection(item); 90 | 91 | if (!_itemSet.Add(item)) 92 | throw new ArgumentException(); 93 | 94 | item.Collection = this; 95 | base.InsertItem(index, item); 96 | } 97 | 98 | /// 99 | /// 100 | /// 101 | /// 102 | protected override void RemoveItem(int index) 103 | { 104 | // Disallow collection modification, so that we don't need to copy it 105 | // when binding the TaskDialogPage. 106 | _boundPage?.DenyIfBound(); 107 | 108 | TaskDialogCustomButton oldItem = this[index]; 109 | oldItem.Collection = null; 110 | _itemSet.Remove(oldItem); 111 | base.RemoveItem(index); 112 | } 113 | 114 | /// 115 | /// 116 | /// 117 | protected override void ClearItems() 118 | { 119 | // Disallow collection modification, so that we don't need to copy it 120 | // when binding the TaskDialogPage. 121 | _boundPage?.DenyIfBound(); 122 | 123 | foreach (TaskDialogCustomButton button in this) 124 | button.Collection = null; 125 | 126 | _itemSet.Clear(); 127 | base.ClearItems(); 128 | } 129 | 130 | private void DenyIfHasOtherCollection(TaskDialogCustomButton item) 131 | { 132 | if (item.Collection != null && item.Collection != this) 133 | throw new InvalidOperationException( 134 | "This control is already part of a different collection."); 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /TaskDialog/TaskDialogCustomButtonStyle.cs: -------------------------------------------------------------------------------- 1 | namespace KPreisser.UI 2 | { 3 | /// 4 | /// 5 | /// 6 | public enum TaskDialogCustomButtonStyle 7 | { 8 | /// 9 | /// Custom buttons should be displayed as normal buttons. 10 | /// 11 | Default = 0, 12 | 13 | /// 14 | /// Custom buttons should be displayed as command links. 15 | /// 16 | CommandLinks = 1, 17 | 18 | /// 19 | /// Custom buttons should be displayed as command links, but without an icon. 20 | /// 21 | CommandLinksNoIcon = 2 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /TaskDialog/TaskDialogExpander.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using TaskDialogFlags = KPreisser.UI.TaskDialogNativeMethods.TASKDIALOG_FLAGS; 4 | using TaskDialogTextElement = KPreisser.UI.TaskDialogNativeMethods.TASKDIALOG_ELEMENTS; 5 | 6 | namespace KPreisser.UI 7 | { 8 | /// 9 | /// 10 | /// 11 | public sealed class TaskDialogExpander : TaskDialogControl 12 | { 13 | private string _text; 14 | 15 | private string _expandedButtonText; 16 | 17 | private string _collapsedButtonText; 18 | 19 | private bool _expandFooterArea; 20 | 21 | private bool _expanded; 22 | 23 | /// 24 | /// 25 | /// 26 | public event EventHandler ExpandedChanged; 27 | 28 | /// 29 | /// 30 | /// 31 | public TaskDialogExpander() 32 | : base() 33 | { 34 | } 35 | 36 | /// 37 | /// 38 | /// 39 | /// 40 | public TaskDialogExpander(string text) 41 | : this() 42 | { 43 | _text = text; 44 | } 45 | 46 | /// 47 | /// Gets or sets the text to be displayed in the dialog's expanded area. 48 | /// 49 | /// 50 | /// This property can be set while the dialog is shown. 51 | /// 52 | public string Text 53 | { 54 | get => _text; 55 | 56 | set 57 | { 58 | DenyIfBoundAndNotCreated(); 59 | DenyIfWaitingForInitialization(); 60 | 61 | // Update the text if we are bound. 62 | BoundPage?.BoundTaskDialog.UpdateTextElement( 63 | TaskDialogTextElement.TDE_EXPANDED_INFORMATION, 64 | value); 65 | 66 | _text = value; 67 | } 68 | } 69 | 70 | /// 71 | /// 72 | /// 73 | public string ExpandedButtonText 74 | { 75 | get => _expandedButtonText; 76 | 77 | set 78 | { 79 | DenyIfBound(); 80 | 81 | _expandedButtonText = value; 82 | } 83 | } 84 | 85 | /// 86 | /// 87 | /// 88 | public string CollapsedButtonText 89 | { 90 | get => _collapsedButtonText; 91 | 92 | set 93 | { 94 | DenyIfBound(); 95 | 96 | _collapsedButtonText = value; 97 | } 98 | } 99 | 100 | /// 101 | /// 102 | /// 103 | public bool Expanded 104 | { 105 | get => _expanded; 106 | 107 | set 108 | { 109 | // The Task Dialog doesn't provide a message type to click the expando 110 | // button, so we don't allow to change this property (it will however 111 | // be updated when we receive an ExpandoButtonClicked notification). 112 | // TODO: Should we throw only if the new value is different than the 113 | // old one? 114 | DenyIfBound(); 115 | 116 | _expanded = value; 117 | } 118 | } 119 | 120 | /// 121 | /// 122 | /// 123 | public bool ExpandFooterArea 124 | { 125 | get => _expandFooterArea; 126 | 127 | set 128 | { 129 | DenyIfBound(); 130 | 131 | _expandFooterArea = value; 132 | } 133 | } 134 | 135 | internal override bool IsCreatable 136 | { 137 | get => base.IsCreatable && !TaskDialogPage.IsNativeStringNullOrEmpty(_text); 138 | } 139 | 140 | /// 141 | /// 142 | /// 143 | /// 144 | public override string ToString() 145 | { 146 | return _text ?? base.ToString(); 147 | } 148 | 149 | internal void HandleExpandoButtonClicked(bool expanded) 150 | { 151 | _expanded = expanded; 152 | OnExpandedChanged(EventArgs.Empty); 153 | } 154 | 155 | private protected override TaskDialogFlags BindCore() 156 | { 157 | TaskDialogFlags flags = base.BindCore(); 158 | 159 | if (_expanded) 160 | flags |= TaskDialogFlags.TDF_EXPANDED_BY_DEFAULT; 161 | if (_expandFooterArea) 162 | flags |= TaskDialogFlags.TDF_EXPAND_FOOTER_AREA; 163 | 164 | return flags; 165 | } 166 | 167 | private void OnExpandedChanged(EventArgs e) 168 | { 169 | ExpandedChanged?.Invoke(this, e); 170 | } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /TaskDialog/TaskDialogFooter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using TaskDialogFlags = KPreisser.UI.TaskDialogNativeMethods.TASKDIALOG_FLAGS; 4 | using TaskDialogIconElement = KPreisser.UI.TaskDialogNativeMethods.TASKDIALOG_ICON_ELEMENTS; 5 | using TaskDialogTextElement = KPreisser.UI.TaskDialogNativeMethods.TASKDIALOG_ELEMENTS; 6 | 7 | namespace KPreisser.UI 8 | { 9 | /// 10 | /// 11 | /// 12 | public sealed class TaskDialogFooter : TaskDialogControl 13 | { 14 | private string _text; 15 | 16 | private TaskDialogIcon _icon; 17 | 18 | private bool _boundIconIsFromHandle; 19 | 20 | /// 21 | /// 22 | /// 23 | public TaskDialogFooter() 24 | : base() 25 | { 26 | } 27 | 28 | /// 29 | /// 30 | /// 31 | /// 32 | public TaskDialogFooter(string text) 33 | : this() 34 | { 35 | _text = text; 36 | } 37 | 38 | /// 39 | /// Gets or sets the text to be displayed in the dialog's footer area. 40 | /// 41 | /// 42 | /// This property can be set while the dialog is shown. 43 | /// 44 | public string Text 45 | { 46 | get => _text; 47 | 48 | set 49 | { 50 | DenyIfBoundAndNotCreated(); 51 | DenyIfWaitingForInitialization(); 52 | 53 | // Update the text if we are bound. 54 | BoundPage?.BoundTaskDialog.UpdateTextElement( 55 | TaskDialogTextElement.TDE_FOOTER, 56 | value); 57 | 58 | _text = value; 59 | } 60 | } 61 | 62 | /// 63 | /// Gets or sets the footer icon. 64 | /// 65 | /// 66 | /// This property can be set while the dialog is shown (but in that case, it 67 | /// cannot be switched between instances of 68 | /// and instances of other icon types). 69 | /// 70 | public TaskDialogIcon Icon 71 | { 72 | get => _icon; 73 | 74 | set 75 | { 76 | DenyIfBoundAndNotCreated(); 77 | DenyIfWaitingForInitialization(); 78 | 79 | (IntPtr iconValue, bool? iconIsFromHandle) = 80 | TaskDialogPage.GetIconValue(value); 81 | 82 | // The native task dialog icon cannot be updated from a handle 83 | // type to a non-handle type and vice versa, so we need to throw 84 | // throw in such a case. 85 | if (BoundPage != null && 86 | iconIsFromHandle != null && 87 | iconIsFromHandle != _boundIconIsFromHandle) 88 | throw new InvalidOperationException( 89 | "Cannot update the icon from a handle icon type to a " + 90 | "non-handle icon type, and vice versa."); 91 | 92 | BoundPage?.BoundTaskDialog.UpdateIconElement( 93 | TaskDialogIconElement.TDIE_ICON_FOOTER, 94 | iconValue); 95 | 96 | _icon = value; 97 | } 98 | } 99 | 100 | internal override bool IsCreatable 101 | { 102 | get => base.IsCreatable && !TaskDialogPage.IsNativeStringNullOrEmpty(_text); 103 | } 104 | 105 | /// 106 | /// 107 | /// 108 | /// 109 | public override string ToString() 110 | { 111 | return _text ?? base.ToString(); 112 | } 113 | 114 | internal TaskDialogFlags Bind(TaskDialogPage page, out IntPtr footerIconValue) 115 | { 116 | TaskDialogFlags result = base.Bind(page); 117 | 118 | footerIconValue = TaskDialogPage.GetIconValue(_icon).iconValue; 119 | 120 | return result; 121 | } 122 | 123 | private protected override TaskDialogFlags BindCore() 124 | { 125 | TaskDialogFlags flags = base.BindCore(); 126 | 127 | _boundIconIsFromHandle = TaskDialogPage.GetIconValue(_icon).iconIsFromHandle 128 | ?? false; 129 | 130 | if (_boundIconIsFromHandle) 131 | flags |= TaskDialogFlags.TDF_USE_HICON_FOOTER; 132 | 133 | return flags; 134 | } 135 | 136 | private protected override void UnbindCore() 137 | { 138 | _boundIconIsFromHandle = false; 139 | 140 | base.UnbindCore(); 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /TaskDialog/TaskDialogHyperlinkClickedEventArgs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace KPreisser.UI 4 | { 5 | /// 6 | /// 7 | /// 8 | public class TaskDialogHyperlinkClickedEventArgs : EventArgs 9 | { 10 | /// 11 | /// 12 | /// 13 | /// 14 | internal TaskDialogHyperlinkClickedEventArgs(string hyperlink) 15 | : base() 16 | { 17 | Hyperlink = hyperlink; 18 | } 19 | 20 | /// 21 | /// 22 | /// 23 | public string Hyperlink 24 | { 25 | get; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /TaskDialog/TaskDialogIcon.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Drawing; 4 | 5 | namespace KPreisser.UI 6 | { 7 | /// 8 | /// 9 | /// 10 | public abstract class TaskDialogIcon 11 | { 12 | private static readonly IReadOnlyDictionary s_standardIcons 13 | = new Dictionary() { 14 | { TaskDialogStandardIcon.None, new TaskDialogStandardIconContainer(TaskDialogStandardIcon.None) }, 15 | { TaskDialogStandardIcon.Information, new TaskDialogStandardIconContainer(TaskDialogStandardIcon.Information) }, 16 | { TaskDialogStandardIcon.Warning, new TaskDialogStandardIconContainer(TaskDialogStandardIcon.Warning) }, 17 | { TaskDialogStandardIcon.Error, new TaskDialogStandardIconContainer(TaskDialogStandardIcon.Error) }, 18 | { TaskDialogStandardIcon.SecurityShield, new TaskDialogStandardIconContainer(TaskDialogStandardIcon.SecurityShield) }, 19 | { TaskDialogStandardIcon.SecurityShieldBlueBar, new TaskDialogStandardIconContainer(TaskDialogStandardIcon.SecurityShieldBlueBar) }, 20 | { TaskDialogStandardIcon.SecurityShieldGrayBar, new TaskDialogStandardIconContainer(TaskDialogStandardIcon.SecurityShieldGrayBar) }, 21 | { TaskDialogStandardIcon.SecurityWarningYellowBar, new TaskDialogStandardIconContainer(TaskDialogStandardIcon.SecurityWarningYellowBar) }, 22 | { TaskDialogStandardIcon.SecurityErrorRedBar, new TaskDialogStandardIconContainer(TaskDialogStandardIcon.SecurityErrorRedBar) }, 23 | { TaskDialogStandardIcon.SecuritySuccessGreenBar, new TaskDialogStandardIconContainer(TaskDialogStandardIcon.SecuritySuccessGreenBar) }, 24 | }; 25 | 26 | private protected TaskDialogIcon() 27 | : base() 28 | { 29 | } 30 | 31 | /// 32 | /// 33 | /// 34 | /// 35 | public static implicit operator TaskDialogIcon(TaskDialogStandardIcon icon) 36 | { 37 | if (!s_standardIcons.TryGetValue(icon, out TaskDialogStandardIconContainer result)) 38 | throw new InvalidCastException(); // TODO: Is this the correct exception type? 39 | 40 | return result; 41 | } 42 | 43 | #if !NET_STANDARD 44 | /// 45 | /// 46 | /// 47 | /// 48 | public static implicit operator TaskDialogIcon(Icon icon) 49 | { 50 | return new TaskDialogIconHandle(icon); 51 | } 52 | #endif 53 | 54 | /// 55 | /// 56 | /// 57 | /// 58 | /// 59 | public static TaskDialogIcon Get(TaskDialogStandardIcon icon) 60 | { 61 | if (!s_standardIcons.TryGetValue(icon, out TaskDialogStandardIconContainer result)) 62 | throw new ArgumentOutOfRangeException(nameof(icon)); 63 | 64 | return result; 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /TaskDialog/TaskDialogIconHandle.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Drawing; 3 | 4 | namespace KPreisser.UI 5 | { 6 | /// 7 | /// 8 | /// 9 | public class TaskDialogIconHandle : TaskDialogIcon 10 | { 11 | /// 12 | /// 13 | /// 14 | /// 15 | public TaskDialogIconHandle(IntPtr iconHandle) 16 | { 17 | IconHandle = iconHandle; 18 | } 19 | 20 | #if !NET_STANDARD 21 | /// 22 | /// 23 | /// 24 | /// 25 | public TaskDialogIconHandle(Icon icon) 26 | : this(icon?.Handle ?? default) 27 | { 28 | } 29 | #endif 30 | 31 | /// 32 | /// 33 | /// 34 | public IntPtr IconHandle 35 | { 36 | get; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /TaskDialog/TaskDialogNativeMethods.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace KPreisser.UI 5 | { 6 | internal static class TaskDialogNativeMethods 7 | { 8 | public const int WM_USER = 0x0400; 9 | 10 | public const int WM_APP = 0x8000; 11 | 12 | public const int WM_ACTIVATE = 0x0006; 13 | 14 | public const int WM_NCACTIVATE = 0x0086; 15 | 16 | public const int WM_WINDOWPOSCHANGING = 0x0046; 17 | 18 | public const int WM_WINDOWPOSCHANGED = 0x0047; 19 | 20 | public const int WA_INACTIVE = 0; 21 | 22 | 23 | //// HResult codes 24 | 25 | #pragma warning disable IDE1006 // Naming Styles 26 | public const int S_OK = 0x0; 27 | 28 | public const int S_FALSE = 0x1; 29 | #pragma warning restore IDE1006 // Naming Styles 30 | 31 | //// Progress Bar states 32 | 33 | public const int PBST_NORMAL = 0x0001; 34 | 35 | public const int PBST_ERROR = 0x0002; 36 | 37 | public const int PBST_PAUSED = 0x0003; 38 | 39 | //// Dialog Box Command IDs 40 | 41 | public const int IDOK = 1; 42 | 43 | public const int IDCANCEL = 2; 44 | 45 | public const int IDABORT = 3; 46 | 47 | public const int IDRETRY = 4; 48 | 49 | public const int IDIGNORE = 5; 50 | 51 | public const int IDYES = 6; 52 | 53 | public const int IDNO = 7; 54 | 55 | public const int IDCLOSE = 8; 56 | 57 | public const int IDHELP = 9; 58 | 59 | public const int IDTRYAGAIN = 10; 60 | 61 | public const int IDCONTINUE = 11; 62 | 63 | [StructLayout(LayoutKind.Sequential)] 64 | public struct WINDOWPOS 65 | { 66 | public IntPtr hwnd; 67 | public IntPtr hwndInsertAfter; 68 | public int x; 69 | public int y; 70 | public int cx; 71 | public int cy; 72 | public WINDOWPOS_FLAGS flags; 73 | } 74 | 75 | [Flags] 76 | public enum WINDOWPOS_FLAGS : int 77 | { 78 | SWP_NOSIZE = 0x0001, 79 | 80 | SWP_NOMOVE = 0x0002, 81 | 82 | SWP_NOZORDER = 0x0004, 83 | 84 | SWP_NOREDRAW = 0x0008, 85 | 86 | SWP_NOACTIVATE = 0x0010, 87 | 88 | SWP_FRAMECHANGED = 0x0020, /* The frame changed: send WM_NCCALCSIZE */ 89 | 90 | SWP_SHOWWINDOW = 0x0040, 91 | 92 | SWP_HIDEWINDOW = 0x0080, 93 | 94 | SWP_NOCOPYBITS = 0x0100, 95 | 96 | SWP_NOOWNERZORDER = 0x0200, /* Don't do owner Z ordering */ 97 | 98 | SWP_NOSENDCHANGING = 0x0400, /* Don't send WM_WINDOWPOSCHANGING */ 99 | 100 | SWP_DEFERERASE = 0x2000, 101 | 102 | SWP_ASYNCWINDOWPOS = 0x4000 103 | } 104 | 105 | //// Note: The TaskDialog declarations (including quoted comments) 106 | //// were taken from CommCtrl.h. 107 | 108 | [Flags] 109 | public enum TASKDIALOG_FLAGS : int 110 | { 111 | TDF_ENABLE_HYPERLINKS = 0x0001, 112 | 113 | TDF_USE_HICON_MAIN = 0x0002, 114 | 115 | TDF_USE_HICON_FOOTER = 0x0004, 116 | 117 | TDF_ALLOW_DIALOG_CANCELLATION = 0x0008, 118 | 119 | TDF_USE_COMMAND_LINKS = 0x0010, 120 | 121 | TDF_USE_COMMAND_LINKS_NO_ICON = 0x0020, 122 | 123 | TDF_EXPAND_FOOTER_AREA = 0x0040, 124 | 125 | TDF_EXPANDED_BY_DEFAULT = 0x0080, 126 | 127 | TDF_VERIFICATION_FLAG_CHECKED = 0x0100, 128 | 129 | TDF_SHOW_PROGRESS_BAR = 0x0200, 130 | 131 | TDF_SHOW_MARQUEE_PROGRESS_BAR = 0x0400, 132 | 133 | TDF_CALLBACK_TIMER = 0x0800, 134 | 135 | TDF_POSITION_RELATIVE_TO_WINDOW = 0x1000, 136 | 137 | TDF_RTL_LAYOUT = 0x2000, 138 | 139 | TDF_NO_DEFAULT_RADIO_BUTTON = 0x4000, 140 | 141 | TDF_CAN_BE_MINIMIZED = 0x8000, 142 | 143 | /// 144 | /// "Don't call SetForegroundWindow() when activating the dialog" 145 | /// 146 | /// 147 | /// This flag is available on Windows NT 6.2 ("Windows 8") and higher. 148 | /// 149 | TDF_NO_SET_FOREGROUND = 0x00010000, 150 | 151 | /// 152 | /// "used by ShellMessageBox to emulate MessageBox sizing behavior" 153 | /// 154 | TDF_SIZE_TO_CONTENT = 0x01000000 155 | } 156 | 157 | public enum TASKDIALOG_MESSAGES : int 158 | { 159 | TDM_NAVIGATE_PAGE = WM_USER + 101, 160 | 161 | /// 162 | /// "wParam = Button ID" 163 | /// 164 | TDM_CLICK_BUTTON = WM_USER + 102, 165 | 166 | /// 167 | /// "wParam = 0 (nonMarque) wParam != 0 (Marquee)" 168 | /// 169 | TDM_SET_MARQUEE_PROGRESS_BAR = WM_USER + 103, 170 | 171 | /// 172 | /// "wParam = new progress state" 173 | /// 174 | TDM_SET_PROGRESS_BAR_STATE = WM_USER + 104, 175 | 176 | /// 177 | /// "lParam = MAKELPARAM(nMinRange, nMaxRange)" 178 | /// 179 | TDM_SET_PROGRESS_BAR_RANGE = WM_USER + 105, 180 | 181 | /// 182 | /// "wParam = new position" 183 | /// 184 | TDM_SET_PROGRESS_BAR_POS = WM_USER + 106, 185 | 186 | /// 187 | /// "wParam = 0 (stop marquee), wParam != 0 (start marquee), 188 | /// lparam = speed (milliseconds between repaints)" 189 | /// 190 | TDM_SET_PROGRESS_BAR_MARQUEE = WM_USER + 107, 191 | 192 | /// 193 | /// "wParam = element (TASKDIALOG_ELEMENTS), lParam = new element text (LPCWSTR)" 194 | /// 195 | TDM_SET_ELEMENT_TEXT = WM_USER + 108, 196 | 197 | /// 198 | /// "wParam = Radio Button ID" 199 | /// 200 | TDM_CLICK_RADIO_BUTTON = WM_USER + 110, 201 | 202 | /// 203 | /// "lParam = 0 (disable), lParam != 0 (enable), wParam = Button ID" 204 | /// 205 | TDM_ENABLE_BUTTON = WM_USER + 111, 206 | 207 | /// 208 | /// "lParam = 0 (disable), lParam != 0 (enable), wParam = Radio Button ID" 209 | /// 210 | TDM_ENABLE_RADIO_BUTTON = WM_USER + 112, 211 | 212 | /// 213 | /// "wParam = 0 (unchecked), 1 (checked), lParam = 1 (set key focus)" 214 | /// 215 | TDM_CLICK_VERIFICATION = WM_USER + 113, 216 | 217 | /// 218 | /// "wParam = element (TASKDIALOG_ELEMENTS), lParam = new element text (LPCWSTR)" 219 | /// 220 | TDM_UPDATE_ELEMENT_TEXT = WM_USER + 114, 221 | 222 | /// 223 | /// "wParam = Button ID, lParam = 0 (elevation not required), 224 | /// lParam != 0 (elevation required)" 225 | /// 226 | TDM_SET_BUTTON_ELEVATION_REQUIRED_STATE = WM_USER + 115, 227 | 228 | /// 229 | /// "wParam = icon element (TASKDIALOG_ICON_ELEMENTS), lParam = new icon 230 | /// (hIcon if TDF_USE_HICON_* was set, PCWSTR otherwise)" 231 | /// 232 | TDM_UPDATE_ICON = WM_USER + 116 233 | } 234 | 235 | public enum TASKDIALOG_NOTIFICATIONS : int 236 | { 237 | TDN_CREATED = 0, 238 | 239 | TDN_NAVIGATED = 1, 240 | 241 | /// 242 | /// "wParam = Button ID" 243 | /// 244 | TDN_BUTTON_CLICKED = 2, 245 | 246 | /// 247 | /// "lParam = (LPCWSTR)pszHREF" 248 | /// 249 | TDN_HYPERLINK_CLICKED = 3, 250 | 251 | /// 252 | /// "wParam = Milliseconds since dialog created or timer reset" 253 | /// 254 | TDN_TIMER = 4, 255 | 256 | TDN_DESTROYED = 5, 257 | 258 | /// 259 | /// "wParam = Radio Button ID" 260 | /// 261 | TDN_RADIO_BUTTON_CLICKED = 6, 262 | 263 | TDN_DIALOG_CONSTRUCTED = 7, 264 | 265 | /// 266 | /// "wParam = 1 if checkbox checked, 0 if not, lParam is unused and always 0" 267 | /// 268 | TDN_VERIFICATION_CLICKED = 8, 269 | 270 | TDN_HELP = 9, 271 | 272 | /// 273 | /// "wParam = 0 (dialog is now collapsed), wParam != 0 (dialog is now expanded)" 274 | /// 275 | TDN_EXPANDO_BUTTON_CLICKED = 10 276 | } 277 | 278 | public enum TASKDIALOG_ELEMENTS 279 | { 280 | TDE_CONTENT, 281 | 282 | TDE_EXPANDED_INFORMATION, 283 | 284 | TDE_FOOTER, 285 | 286 | TDE_MAIN_INSTRUCTION 287 | } 288 | 289 | public enum TASKDIALOG_ICON_ELEMENTS 290 | { 291 | TDIE_ICON_MAIN, 292 | 293 | TDIE_ICON_FOOTER 294 | } 295 | 296 | // Packing is defined as 1 in CommCtrl.h ("pack(1)"). 297 | [StructLayout(LayoutKind.Sequential, Pack = 1)] 298 | public struct TASKDIALOGCONFIG 299 | { 300 | public uint cbSize; 301 | /// 302 | /// "incorrectly named, this is the owner window, not a parent." 303 | /// 304 | public IntPtr hwndParent; 305 | /// 306 | /// "used for MAKEINTRESOURCE() strings" 307 | /// 308 | public IntPtr hInstance; 309 | public TASKDIALOG_FLAGS dwFlags; 310 | public TaskDialogButtons dwCommonButtons; 311 | public IntPtr pszWindowTitle; 312 | public IntPtr mainIconUnion; 313 | public IntPtr pszMainInstruction; 314 | public IntPtr pszContent; 315 | public uint cButtons; 316 | public IntPtr pButtons; 317 | public int nDefaultButton; 318 | public uint cRadioButtons; 319 | public IntPtr pRadioButtons; 320 | public int nDefaultRadioButton; 321 | public IntPtr pszVerificationText; 322 | public IntPtr pszExpandedInformation; 323 | public IntPtr pszExpandedControlText; 324 | public IntPtr pszCollapsedControlText; 325 | public IntPtr footerIconUnion; 326 | public IntPtr pszFooter; 327 | public IntPtr pfCallback; 328 | public IntPtr lpCallbackData; 329 | /// 330 | /// "width of the Task Dialog's client area in DLU's. If 0, Task Dialog 331 | /// will calculate the ideal width." 332 | /// 333 | public uint cxWidth; 334 | } 335 | 336 | // Packing is defined as 1 in CommCtrl.h ("pack(1)"). 337 | [StructLayout(LayoutKind.Sequential, Pack = 1)] 338 | public struct TASKDIALOG_BUTTON 339 | { 340 | public int nButtonID; 341 | public IntPtr pszButtonText; 342 | } 343 | 344 | [UnmanagedFunctionPointer(CallingConvention.StdCall)] 345 | public delegate int PFTASKDIALOGCALLBACK( 346 | IntPtr hwnd, 347 | TASKDIALOG_NOTIFICATIONS msg, 348 | IntPtr wParam, 349 | IntPtr lParam, 350 | IntPtr lpRefData); 351 | 352 | [DllImport("comctl32", 353 | EntryPoint = "TaskDialogIndirect", 354 | ExactSpelling = true, 355 | SetLastError = true)] 356 | public static extern int TaskDialogIndirect( 357 | IntPtr pTaskConfig, 358 | [Out] out int pnButton, 359 | [Out] out int pnRadioButton, 360 | [MarshalAs(UnmanagedType.Bool), Out] out bool pfVerificationFlagChecked); 361 | 362 | [DllImport("user32", 363 | EntryPoint = "SendMessageW", 364 | ExactSpelling = true, 365 | SetLastError = true)] 366 | public static extern IntPtr SendMessage( 367 | IntPtr hWnd, 368 | int Msg, 369 | IntPtr wParam, 370 | IntPtr lParam); 371 | 372 | [DllImport("user32", 373 | EntryPoint = "PostMessageW", 374 | ExactSpelling = true, 375 | SetLastError = true)] 376 | public static extern bool PostMessage( 377 | IntPtr hWnd, 378 | int Msg, 379 | IntPtr wParam, 380 | IntPtr lParam); 381 | 382 | [DllImport("user32", 383 | CharSet = CharSet.Unicode, 384 | EntryPoint = "SetWindowTextW", 385 | ExactSpelling = true, 386 | SetLastError = true)] 387 | public static extern bool SetWindowText( 388 | IntPtr hWnd, 389 | string lpString); 390 | 391 | [DllImport("user32", 392 | EntryPoint = "GetForegroundWindow", 393 | ExactSpelling = true)] 394 | public static extern IntPtr GetForegroundWindow(); 395 | 396 | [DllImport("user32", 397 | EntryPoint = "GetActiveWindow", 398 | ExactSpelling = true)] 399 | public static extern IntPtr GetActiveWindow(); 400 | } 401 | } 402 | -------------------------------------------------------------------------------- /TaskDialog/TaskDialogPage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | using TaskDialogFlags = KPreisser.UI.TaskDialogNativeMethods.TASKDIALOG_FLAGS; 6 | using TaskDialogIconElement = KPreisser.UI.TaskDialogNativeMethods.TASKDIALOG_ICON_ELEMENTS; 7 | using TaskDialogTextElement = KPreisser.UI.TaskDialogNativeMethods.TASKDIALOG_ELEMENTS; 8 | 9 | namespace KPreisser.UI 10 | { 11 | /// 12 | /// 13 | /// 14 | public class TaskDialogPage 15 | { 16 | /// 17 | /// The start ID for custom buttons. 18 | /// 19 | /// 20 | /// We need to ensure we don't use a ID that is already used for a 21 | /// standard button (TaskDialogResult), so we start with 100 to be safe 22 | /// (100 is also used as first ID in MSDN examples for the task dialog). 23 | /// 24 | private const int CustomButtonStartID = 100; 25 | 26 | /// 27 | /// The start ID for radio buttons. 28 | /// 29 | /// 30 | /// This must be at least 1 because 0 already stands for "no button". 31 | /// 32 | private const int RadioButtonStartID = 1; 33 | 34 | private TaskDialogStandardButtonCollection _standardButtons; 35 | 36 | private TaskDialogCustomButtonCollection _customButtons; 37 | 38 | private TaskDialogRadioButtonCollection _radioButtons; 39 | 40 | private TaskDialogCheckBox _checkBox; 41 | 42 | private TaskDialogExpander _expander; 43 | 44 | private TaskDialogFooter _footer; 45 | 46 | private TaskDialogProgressBar _progressBar; 47 | 48 | private TaskDialogFlags _flags; 49 | private string _title; 50 | private string _instruction; 51 | private string _text; 52 | private TaskDialogIcon _icon; 53 | private int _width; 54 | private TaskDialogCustomButtonStyle _customButtonStyle; 55 | 56 | private TaskDialog _boundTaskDialog; 57 | 58 | private bool _boundIconIsFromHandle; 59 | 60 | private bool _appliedInitialization; 61 | 62 | /// 63 | /// Occurs after this instance is bound to a task dialog and the task dialog 64 | /// has created the GUI elements represented by this 65 | /// instance. 66 | /// 67 | /// 68 | /// This will happen after showing or navigating the dialog. 69 | /// 70 | public event EventHandler Created; 71 | 72 | /// 73 | /// Occurs when the task dialog is about to destroy the GUI elements represented 74 | /// by this instance and it is about to be 75 | /// unbound from the task dialog. 76 | /// 77 | /// 78 | /// This will happen when closing or navigating the dialog. 79 | /// 80 | public event EventHandler Destroyed; 81 | 82 | /// 83 | /// Occurs when the user presses F1 while the task dialog has focus, or when the 84 | /// user clicks the button. 85 | /// 86 | public event EventHandler Help; 87 | 88 | /// 89 | /// 90 | /// 91 | public event EventHandler HyperlinkClicked; 92 | 93 | /// 94 | /// 95 | /// 96 | public TaskDialogPage() 97 | { 98 | // Create empty (hidden) controls. 99 | _checkBox = new TaskDialogCheckBox(); 100 | _expander = new TaskDialogExpander(); 101 | _footer = new TaskDialogFooter(); 102 | _progressBar = new TaskDialogProgressBar(TaskDialogProgressBarState.None); 103 | } 104 | 105 | /// 106 | /// 107 | /// 108 | public TaskDialogStandardButtonCollection StandardButtons 109 | { 110 | get => _standardButtons ?? 111 | (_standardButtons = new TaskDialogStandardButtonCollection()); 112 | 113 | set 114 | { 115 | // We must deny this if we are bound because we need to be able to 116 | // access the controls from the task dialog's callback. 117 | DenyIfBound(); 118 | 119 | _standardButtons = value ?? throw new ArgumentNullException(nameof(value)); 120 | } 121 | } 122 | 123 | /// 124 | /// 125 | /// 126 | public TaskDialogCustomButtonCollection CustomButtons 127 | { 128 | get => _customButtons ?? 129 | (_customButtons = new TaskDialogCustomButtonCollection()); 130 | 131 | set 132 | { 133 | // We must deny this if we are bound because we need to be able to 134 | // access the controls from the task dialog's callback. 135 | DenyIfBound(); 136 | 137 | _customButtons = value ?? throw new ArgumentNullException(nameof(value)); 138 | } 139 | } 140 | 141 | /// 142 | /// 143 | /// 144 | public TaskDialogRadioButtonCollection RadioButtons 145 | { 146 | get => _radioButtons ?? 147 | (_radioButtons = new TaskDialogRadioButtonCollection()); 148 | 149 | set 150 | { 151 | // We must deny this if we are bound because we need to be able to 152 | // access the controls from the task dialog's callback. 153 | DenyIfBound(); 154 | 155 | _radioButtons = value ?? throw new ArgumentNullException(nameof(value)); 156 | } 157 | } 158 | 159 | /// 160 | /// 161 | /// 162 | public TaskDialogCheckBox CheckBox 163 | { 164 | get => _checkBox; 165 | 166 | set 167 | { 168 | // We must deny this if we are bound because we need to be able to 169 | // access the control from the task dialog's callback. 170 | DenyIfBound(); 171 | 172 | _checkBox = value; 173 | } 174 | } 175 | 176 | /// 177 | /// 178 | /// 179 | public TaskDialogExpander Expander 180 | { 181 | get => _expander; 182 | 183 | set 184 | { 185 | // We must deny this if we are bound because we need to be able to 186 | // access the control from the task dialog's callback. 187 | DenyIfBound(); 188 | 189 | _expander = value; 190 | } 191 | } 192 | 193 | /// 194 | /// 195 | /// 196 | public TaskDialogFooter Footer 197 | { 198 | get => _footer; 199 | 200 | set 201 | { 202 | // We must deny this if we are bound because we need to be able to 203 | // access the control from the task dialog's callback. 204 | DenyIfBound(); 205 | 206 | _footer = value; 207 | } 208 | } 209 | 210 | /// 211 | /// 212 | /// 213 | public TaskDialogProgressBar ProgressBar 214 | { 215 | get => _progressBar; 216 | 217 | set 218 | { 219 | // We must deny this if we are bound because we need to be able to 220 | // access the control from the task dialog's callback. 221 | DenyIfBound(); 222 | 223 | _progressBar = value; 224 | } 225 | } 226 | 227 | /// 228 | /// Gets or sets the title of the task dialog window. 229 | /// 230 | /// 231 | /// This property can be set while the dialog is shown. 232 | /// 233 | public string Title 234 | { 235 | get => _title; 236 | 237 | set 238 | { 239 | DenyIfWaitingForInitialization(); 240 | 241 | // Note: We set the field values after calling the method to ensure 242 | // it still has the previous value it the method throws. 243 | _boundTaskDialog?.UpdateTitle(value); 244 | 245 | _title = value; 246 | } 247 | } 248 | 249 | /// 250 | /// Gets or sets the main instruction text. 251 | /// 252 | /// 253 | /// This property can be set while the dialog is shown. 254 | /// 255 | public string Instruction 256 | { 257 | get => _instruction; 258 | 259 | set 260 | { 261 | DenyIfWaitingForInitialization(); 262 | 263 | _boundTaskDialog?.UpdateTextElement( 264 | TaskDialogTextElement.TDE_MAIN_INSTRUCTION, 265 | value); 266 | 267 | _instruction = value; 268 | } 269 | } 270 | 271 | /// 272 | /// Gets or sets the dialog's primary text content. 273 | /// 274 | /// 275 | /// This property can be set while the dialog is shown. 276 | /// 277 | public string Text 278 | { 279 | get => _text; 280 | 281 | set 282 | { 283 | DenyIfWaitingForInitialization(); 284 | 285 | _boundTaskDialog?.UpdateTextElement( 286 | TaskDialogTextElement.TDE_CONTENT, 287 | value); 288 | 289 | _text = value; 290 | } 291 | } 292 | 293 | /// 294 | /// Gets or sets the main icon. 295 | /// 296 | /// 297 | /// This property can be set while the dialog is shown (but in that case, it 298 | /// cannot be switched between instances of 299 | /// and instances of other icon types). 300 | /// 301 | public TaskDialogIcon Icon 302 | { 303 | get => _icon; 304 | 305 | set 306 | { 307 | DenyIfWaitingForInitialization(); 308 | 309 | (IntPtr iconValue, bool? iconIsFromHandle) = GetIconValue(value); 310 | 311 | // The native task dialog icon cannot be updated from a handle 312 | // type to a non-handle type and vice versa, so we need to throw 313 | // throw in such a case. 314 | if (_boundTaskDialog != null && 315 | iconIsFromHandle != null && 316 | iconIsFromHandle != _boundIconIsFromHandle) 317 | throw new InvalidOperationException( 318 | "Cannot update the icon from a handle icon type to a " + 319 | "non-handle icon type, and vice versa."); 320 | 321 | _boundTaskDialog?.UpdateIconElement( 322 | TaskDialogIconElement.TDIE_ICON_MAIN, 323 | iconValue); 324 | 325 | _icon = value; 326 | } 327 | } 328 | 329 | /// 330 | /// Gets or sets the width in dialog units that the dialog's client area will get 331 | /// when the dialog is is created or navigated. 332 | /// If 0, the width will be automatically calculated by the system. 333 | /// 334 | public int Width 335 | { 336 | get => _width; 337 | 338 | set 339 | { 340 | if (value < 0) 341 | throw new ArgumentOutOfRangeException(nameof(value)); 342 | 343 | DenyIfBound(); 344 | 345 | _width = value; 346 | } 347 | } 348 | 349 | /// 350 | /// Gets or sets the that specifies how to 351 | /// display custom buttons. 352 | /// 353 | public TaskDialogCustomButtonStyle CustomButtonStyle 354 | { 355 | get => _customButtonStyle; 356 | 357 | set 358 | { 359 | DenyIfBound(); 360 | 361 | _customButtonStyle = value; 362 | } 363 | } 364 | 365 | /// 366 | /// 367 | /// 368 | public bool EnableHyperlinks 369 | { 370 | get => GetFlag(TaskDialogFlags.TDF_ENABLE_HYPERLINKS); 371 | set => SetFlag(TaskDialogFlags.TDF_ENABLE_HYPERLINKS, value); 372 | } 373 | 374 | /// 375 | /// Gets or sets a value that indicates whether the task dialog can be canceled 376 | /// by pressing ESC, Alt+F4 or clicking the title bar's close button even if no 377 | /// button is specified in 378 | /// . 379 | /// 380 | /// 381 | /// You can intercept cancellation of the dialog without displaying a "Cancel" 382 | /// button by adding a with its 383 | /// set to false and specifying 384 | /// a result. 385 | /// 386 | public bool AllowCancel 387 | { 388 | get => GetFlag(TaskDialogFlags.TDF_ALLOW_DIALOG_CANCELLATION); 389 | set => SetFlag(TaskDialogFlags.TDF_ALLOW_DIALOG_CANCELLATION, value); 390 | } 391 | 392 | /// 393 | /// 394 | /// 395 | /// 396 | /// Note that once a task dialog has been opened with or has navigated to a 397 | /// where this flag is set, it will keep on 398 | /// subsequent navigations to a new even when 399 | /// it doesn't have this flag set. 400 | /// 401 | public bool RightToLeftLayout 402 | { 403 | get => GetFlag(TaskDialogFlags.TDF_RTL_LAYOUT); 404 | set => SetFlag(TaskDialogFlags.TDF_RTL_LAYOUT, value); 405 | } 406 | 407 | /// 408 | /// Gets or sets a value that indicates whether the task dialog can be minimized 409 | /// when it is shown modeless. 410 | /// 411 | /// 412 | /// When setting this property to true, is 413 | /// automatically implied. 414 | /// 415 | public bool CanBeMinimized 416 | { 417 | get => GetFlag(TaskDialogFlags.TDF_CAN_BE_MINIMIZED); 418 | set => SetFlag(TaskDialogFlags.TDF_CAN_BE_MINIMIZED, value); 419 | } 420 | 421 | /// 422 | /// Indicates that the width of the task dialog is determined by the width 423 | /// of its content area (similar to Message Box sizing behavior). 424 | /// 425 | /// 426 | /// This flag is ignored if is not set to 0. 427 | /// 428 | public bool SizeToContent 429 | { 430 | get => GetFlag(TaskDialogFlags.TDF_SIZE_TO_CONTENT); 431 | set => SetFlag(TaskDialogFlags.TDF_SIZE_TO_CONTENT, value); 432 | } 433 | 434 | internal TaskDialog BoundTaskDialog 435 | { 436 | get => _boundTaskDialog; 437 | } 438 | 439 | /// 440 | /// Gets a value that indicates if the 441 | /// started navigation to this page but navigation did not yet complete 442 | /// (in which case we cannot modify the dialog even though we are bound). 443 | /// 444 | internal bool WaitingForInitialization 445 | { 446 | get => _boundTaskDialog != null && !_appliedInitialization; 447 | } 448 | 449 | internal static bool IsNativeStringNullOrEmpty(string str) 450 | { 451 | // From a native point of view, the string is empty if its first 452 | // character is a NUL char. 453 | return string.IsNullOrEmpty(str) || str[0] == '\0'; 454 | } 455 | 456 | internal static (IntPtr iconValue, bool? iconIsFromHandle) GetIconValue( 457 | TaskDialogIcon icon) 458 | { 459 | IntPtr iconValue = default; 460 | bool? iconIsFromHandle = null; 461 | 462 | // If no icon is specified (icon is null), we don't set the 463 | // "iconIsFromHandle" flag, which means that the icon can be updated 464 | // to show a Standard icon while the dialog is running. 465 | if (icon is TaskDialogIconHandle iconHandle) 466 | { 467 | iconIsFromHandle = true; 468 | iconValue = iconHandle.IconHandle; 469 | } 470 | else if (icon is TaskDialogStandardIconContainer standardIconContainer) 471 | { 472 | iconIsFromHandle = false; 473 | iconValue = (IntPtr)standardIconContainer.Icon; 474 | } 475 | 476 | return (iconValue, iconIsFromHandle); 477 | } 478 | 479 | internal void DenyIfBound() 480 | { 481 | if (_boundTaskDialog != null) 482 | throw new InvalidOperationException( 483 | "Cannot set this property or call this method while the " + 484 | "page is bound to a task dialog."); 485 | } 486 | 487 | internal void DenyIfWaitingForInitialization() 488 | { 489 | if (WaitingForInitialization) 490 | throw new InvalidOperationException( 491 | $"Navigation of the task dialog did not complete yet. " + 492 | $"Please wait for the " + 493 | $"{nameof(TaskDialogPage)}.{nameof(Created)} event to occur."); 494 | } 495 | 496 | internal TaskDialogButton GetBoundButtonByID(int buttonID) 497 | { 498 | if (_boundTaskDialog == null) 499 | throw new InvalidOperationException(); 500 | 501 | if (buttonID == 0) 502 | return null; 503 | 504 | // Check if the button is part of the custom buttons. 505 | var button = null as TaskDialogButton; 506 | if (buttonID >= CustomButtonStartID) 507 | { 508 | button = _customButtons[buttonID - CustomButtonStartID]; 509 | } 510 | else 511 | { 512 | // Note: We deliberately return null instead of throwing when 513 | // the common button ID is not part of the collection, because 514 | // the caller might not know if such a button exists. 515 | var result = (TaskDialogResult)buttonID; 516 | if (_standardButtons.Contains(result)) 517 | button = _standardButtons[result]; 518 | } 519 | 520 | return button; 521 | } 522 | 523 | internal TaskDialogRadioButton GetBoundRadioButtonByID(int buttonID) 524 | { 525 | if (_boundTaskDialog == null) 526 | throw new InvalidOperationException(); 527 | 528 | if (buttonID == 0) 529 | return null; 530 | 531 | return _radioButtons[buttonID - RadioButtonStartID]; 532 | } 533 | 534 | internal void Validate() 535 | { 536 | //// Before assigning button IDs etc., check if the button configs are OK. 537 | //// This needs to be done before clearing the old button IDs and assigning 538 | //// the new ones, because it is possible to use the same button 539 | //// instances after a dialog has been created for Navigate(), where need to 540 | //// do the check, then release the old buttons, then assign the new 541 | //// buttons. 542 | 543 | // Check that this page instance is not already bound to a 544 | // TaskDialog instance. 545 | if (_boundTaskDialog != null) 546 | throw new InvalidOperationException( 547 | $"This {nameof(TaskDialogPage)} instance is already bound to " + 548 | $"a {nameof(TaskDialog)} instance."); 549 | 550 | // We also need to validate the controls since they could also be assigned to 551 | // another (bound) TaskDialogPage at the same time. 552 | // Access the collections using the property to ensure they exist. 553 | if (StandardButtons.BoundPage != null || 554 | CustomButtons.BoundPage != null || 555 | RadioButtons.BoundPage != null || 556 | _checkBox?.BoundPage != null || 557 | _expander?.BoundPage != null || 558 | _footer?.BoundPage != null || 559 | _progressBar?.BoundPage != null) 560 | throw new InvalidOperationException(); 561 | 562 | foreach (TaskDialogControl control in (StandardButtons as IEnumerable) 563 | .Concat(CustomButtons) 564 | .Concat(RadioButtons)) 565 | if (control.BoundPage != null) 566 | throw new InvalidOperationException(); 567 | 568 | if (CustomButtons.Count > int.MaxValue - CustomButtonStartID + 1 || 569 | RadioButtons.Count > int.MaxValue - RadioButtonStartID + 1) 570 | throw new InvalidOperationException( 571 | "Too many custom buttons or radio buttons have been added."); 572 | 573 | bool foundDefaultButton = false; 574 | foreach (TaskDialogButton button in (StandardButtons as IEnumerable) 575 | .Concat(CustomButtons)) 576 | { 577 | if (button.DefaultButton) 578 | { 579 | if (!foundDefaultButton) 580 | foundDefaultButton = true; 581 | else 582 | throw new InvalidOperationException( 583 | "Only one button can be set as default button."); 584 | } 585 | } 586 | 587 | // For custom and radio buttons, we need to ensure the strings are 588 | // not null or empty, as otherwise an error would occur when 589 | // showing/navigating the dialog. 590 | foreach (TaskDialogCustomButton button in _customButtons) 591 | { 592 | if (!button.IsCreatable) 593 | throw new InvalidOperationException( 594 | "The text of a custom button must not be null or empty."); 595 | } 596 | 597 | bool foundCheckedRadioButton = false; 598 | foreach (TaskDialogRadioButton button in _radioButtons) 599 | { 600 | if (!button.IsCreatable) 601 | throw new InvalidOperationException( 602 | "The text of a radio button must not be null or empty."); 603 | 604 | if (button.Checked) 605 | { 606 | if (!foundCheckedRadioButton) 607 | foundCheckedRadioButton = true; 608 | else 609 | throw new InvalidOperationException( 610 | "Only one radio button can be set as checked."); 611 | } 612 | } 613 | } 614 | 615 | internal void Bind( 616 | TaskDialog owner, 617 | out TaskDialogFlags flags, 618 | out TaskDialogButtons buttonFlags, 619 | out IntPtr iconValue, 620 | out IntPtr footerIconValue, 621 | out int defaultButtonID, 622 | out int defaultRadioButtonID) 623 | { 624 | if (_boundTaskDialog != null) 625 | throw new InvalidOperationException(); 626 | 627 | //// This method assumes Validate() has already been called. 628 | 629 | _boundTaskDialog = owner; 630 | flags = _flags; 631 | 632 | (IntPtr localIconValue, bool? iconIsFromHandle) = GetIconValue(_icon); 633 | (iconValue, _boundIconIsFromHandle) = (localIconValue, iconIsFromHandle ?? false); 634 | 635 | if (_boundIconIsFromHandle) 636 | flags |= TaskDialogFlags.TDF_USE_HICON_MAIN; 637 | 638 | // Only specify the command link flags if there actually are custom buttons; 639 | // otherwise the dialog will not work. 640 | if (_customButtons.Count > 0) 641 | { 642 | if (_customButtonStyle == TaskDialogCustomButtonStyle.CommandLinks) 643 | flags |= TaskDialogFlags.TDF_USE_COMMAND_LINKS; 644 | else if (_customButtonStyle == TaskDialogCustomButtonStyle.CommandLinksNoIcon) 645 | flags |= TaskDialogFlags.TDF_USE_COMMAND_LINKS_NO_ICON; 646 | } 647 | 648 | TaskDialogStandardButtonCollection standardButtons = StandardButtons; 649 | TaskDialogCustomButtonCollection customButtons = CustomButtons; 650 | TaskDialogRadioButtonCollection radioButtons = RadioButtons; 651 | 652 | standardButtons.BoundPage = this; 653 | customButtons.BoundPage = this; 654 | radioButtons.BoundPage = this; 655 | 656 | // Assign IDs to the buttons based on their index. 657 | // Note: The collections will be locked while this page is bound, so we 658 | // don't need to copy them here. 659 | defaultButtonID = 0; 660 | buttonFlags = default; 661 | foreach (TaskDialogStandardButton standardButton in standardButtons) 662 | { 663 | flags |= standardButton.Bind(this); 664 | 665 | if (standardButton.IsCreated) 666 | { 667 | buttonFlags |= standardButton.GetButtonFlag(); 668 | 669 | if (standardButton.DefaultButton && defaultButtonID == 0) 670 | defaultButtonID = standardButton.ButtonID; 671 | } 672 | } 673 | 674 | for (int i = 0; i < customButtons.Count; i++) 675 | { 676 | TaskDialogCustomButton customButton = customButtons[i]; 677 | flags |= customButton.Bind(this, CustomButtonStartID + i); 678 | 679 | if (customButton.IsCreated) 680 | { 681 | if (customButton.DefaultButton && defaultButtonID == 0) 682 | defaultButtonID = customButton.ButtonID; 683 | } 684 | } 685 | 686 | defaultRadioButtonID = 0; 687 | for (int i = 0; i < radioButtons.Count; i++) 688 | { 689 | TaskDialogRadioButton radioButton = radioButtons[i]; 690 | flags |= radioButton.Bind(this, RadioButtonStartID + i); 691 | 692 | if (radioButton.IsCreated) 693 | { 694 | if (radioButton.Checked && defaultRadioButtonID == 0) 695 | defaultRadioButtonID = radioButton.RadioButtonID; 696 | else if (radioButton.Checked) 697 | radioButton.Checked = false; 698 | } 699 | } 700 | 701 | if (defaultRadioButtonID == 0) 702 | flags |= TaskDialogFlags.TDF_NO_DEFAULT_RADIO_BUTTON; 703 | 704 | if (_checkBox != null) 705 | flags |= _checkBox.Bind(this); 706 | 707 | if (_expander != null) 708 | flags |= _expander.Bind(this); 709 | 710 | if (_footer != null) 711 | flags |= _footer.Bind(this, out footerIconValue); 712 | else 713 | footerIconValue = default; 714 | 715 | if (_progressBar != null) 716 | flags |= _progressBar.Bind(this); 717 | } 718 | 719 | internal void Unbind() 720 | { 721 | if (_boundTaskDialog == null) 722 | throw new InvalidOperationException(); 723 | 724 | TaskDialogStandardButtonCollection standardButtons = StandardButtons; 725 | TaskDialogCustomButtonCollection customButtons = CustomButtons; 726 | TaskDialogRadioButtonCollection radioButtons = RadioButtons; 727 | 728 | foreach (TaskDialogStandardButton standardButton in standardButtons) 729 | standardButton.Unbind(); 730 | 731 | foreach (TaskDialogCustomButton customButton in customButtons) 732 | customButton.Unbind(); 733 | 734 | foreach (TaskDialogRadioButton radioButton in radioButtons) 735 | radioButton.Unbind(); 736 | 737 | standardButtons.BoundPage = null; 738 | customButtons.BoundPage = null; 739 | radioButtons.BoundPage = null; 740 | 741 | _checkBox?.Unbind(); 742 | _expander?.Unbind(); 743 | _footer?.Unbind(); 744 | _progressBar?.Unbind(); 745 | 746 | _boundTaskDialog = null; 747 | _appliedInitialization = false; 748 | } 749 | 750 | internal void ApplyInitialization() 751 | { 752 | if (_appliedInitialization) 753 | throw new InvalidOperationException(); 754 | 755 | _appliedInitialization = true; 756 | 757 | foreach (TaskDialogStandardButton button in StandardButtons) 758 | button.ApplyInitialization(); 759 | 760 | foreach (TaskDialogCustomButton button in CustomButtons) 761 | button.ApplyInitialization(); 762 | 763 | foreach (TaskDialogRadioButton button in RadioButtons) 764 | button.ApplyInitialization(); 765 | 766 | _checkBox?.ApplyInitialization(); 767 | _expander?.ApplyInitialization(); 768 | _footer?.ApplyInitialization(); 769 | _progressBar?.ApplyInitialization(); 770 | } 771 | 772 | /// 773 | /// 774 | /// 775 | /// 776 | internal protected void OnCreated(EventArgs e) 777 | { 778 | Created?.Invoke(this, e); 779 | } 780 | 781 | /// 782 | /// 783 | /// 784 | /// 785 | internal protected void OnDestroyed(EventArgs e) 786 | { 787 | Destroyed?.Invoke(this, e); 788 | } 789 | 790 | /// 791 | /// 792 | /// 793 | /// 794 | internal protected void OnHelp(EventArgs e) 795 | { 796 | Help?.Invoke(this, e); 797 | } 798 | 799 | /// 800 | /// 801 | /// 802 | /// 803 | internal protected void OnHyperlinkClicked(TaskDialogHyperlinkClickedEventArgs e) 804 | { 805 | HyperlinkClicked?.Invoke(this, e); 806 | } 807 | 808 | private bool GetFlag(TaskDialogFlags flag) 809 | { 810 | return (_flags & flag) == flag; 811 | } 812 | 813 | private void SetFlag(TaskDialogFlags flag, bool value) 814 | { 815 | DenyIfBound(); 816 | 817 | if (value) 818 | _flags |= flag; 819 | else 820 | _flags &= ~flag; 821 | } 822 | } 823 | } 824 | -------------------------------------------------------------------------------- /TaskDialog/TaskDialogProgressBar.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using TaskDialogFlags = KPreisser.UI.TaskDialogNativeMethods.TASKDIALOG_FLAGS; 4 | 5 | namespace KPreisser.UI 6 | { 7 | /// 8 | /// 9 | /// 10 | public sealed class TaskDialogProgressBar : TaskDialogControl 11 | { 12 | private TaskDialogProgressBarState _state; 13 | 14 | private int _minimum = 0; 15 | 16 | private int _maximum = 100; 17 | 18 | private int _value; 19 | 20 | private int _marqueeSpeed; 21 | 22 | /// 23 | /// 24 | /// 25 | public TaskDialogProgressBar() 26 | : base() 27 | { 28 | } 29 | 30 | /// 31 | /// 32 | /// 33 | public TaskDialogProgressBar(TaskDialogProgressBarState state) 34 | : this() 35 | { 36 | // Use the setter which will validate the value. 37 | State = state; 38 | } 39 | 40 | /// 41 | /// Gets or sets the state of the progress bar. 42 | /// 43 | /// 44 | /// This property can be set while the dialog is shown. However, it is 45 | /// not possible to change the state from 46 | /// to any other state, 47 | /// and vice versa. 48 | /// 49 | public TaskDialogProgressBarState State 50 | { 51 | get => _state; 52 | 53 | set 54 | { 55 | DenyIfBoundAndNotCreated(); 56 | DenyIfWaitingForInitialization(); 57 | 58 | if (BoundPage != null && value == TaskDialogProgressBarState.None) 59 | throw new InvalidOperationException( 60 | "Cannot remove the progress bar while the task dialog is shown."); 61 | 62 | //// TODO: Verify the enum value is actually valid 63 | 64 | TaskDialogProgressBarState previousState = _state; 65 | _state = value; 66 | try 67 | { 68 | if (BoundPage != null) 69 | { 70 | TaskDialog taskDialog = BoundPage.BoundTaskDialog; 71 | 72 | // Check if we need to switch between a marquee and a 73 | // non-marquee bar. 74 | bool newStateIsMarquee = ProgressBarStateIsMarquee(_state); 75 | bool switchMode = ProgressBarStateIsMarquee(previousState) != newStateIsMarquee; 76 | if (switchMode) 77 | { 78 | // When switching from non-marquee to marquee mode, we 79 | // first need to set the state to "Normal"; otherwise 80 | // the marquee will not show. 81 | if (newStateIsMarquee && previousState != TaskDialogProgressBarState.Normal) 82 | taskDialog.SetProgressBarState(TaskDialogNativeMethods.PBST_NORMAL); 83 | 84 | taskDialog.SwitchProgressBarMode(newStateIsMarquee); 85 | } 86 | 87 | // Update the properties. 88 | if (newStateIsMarquee) 89 | { 90 | taskDialog.SetProgressBarMarquee( 91 | _state == TaskDialogProgressBarState.Marquee, 92 | _marqueeSpeed); 93 | } 94 | else 95 | { 96 | taskDialog.SetProgressBarState(GetNativeProgressBarState(_state)); 97 | 98 | if (switchMode) 99 | { 100 | // Also need to set the other properties after switching 101 | // the mode. 102 | taskDialog.SetProgressBarRange( 103 | _minimum, 104 | _maximum); 105 | taskDialog.SetProgressBarPosition( 106 | _value); 107 | 108 | // We need to set the position a second time to work 109 | // reliably if the state is not "Normal". 110 | // See this comment in the TaskDialog implementation 111 | // of the Windows API Code Pack 1.1: 112 | // "Due to a bug that wasn't fixed in time for RTM of 113 | // Vista, second SendMessage is required if the state 114 | // is non-Normal." 115 | // Apparently, this bug is still present in Win10 V1803. 116 | if (_state != TaskDialogProgressBarState.Normal) 117 | taskDialog.SetProgressBarPosition( 118 | _value); 119 | } 120 | } 121 | } 122 | } 123 | catch 124 | { 125 | // Revert to the previous state. This could happen if the dialog's 126 | // DenyIfDialogNotShownOrWaitingForNavigatedEvent() (called by 127 | // one of the Set...() methods) throws. 128 | _state = previousState; 129 | throw; 130 | } 131 | } 132 | } 133 | 134 | /// 135 | /// 136 | /// 137 | /// 138 | /// This property can be set while the dialog is shown. 139 | /// 140 | public int Minimum 141 | { 142 | get => _minimum; 143 | 144 | set 145 | { 146 | if (value < 0 || value > ushort.MaxValue) 147 | throw new ArgumentOutOfRangeException(nameof(value)); 148 | 149 | DenyIfBoundAndNotCreated(); 150 | DenyIfWaitingForInitialization(); 151 | 152 | // We only update the TaskDialog if the current state is a 153 | // non-marquee progress bar. 154 | if (BoundPage != null && !ProgressBarStateIsMarquee(_state)) 155 | { 156 | BoundPage.BoundTaskDialog.SetProgressBarRange( 157 | value, 158 | _maximum); 159 | } 160 | 161 | _minimum = value; 162 | } 163 | } 164 | 165 | /// 166 | /// 167 | /// 168 | /// 169 | /// This property can be set while the dialog is shown. 170 | /// 171 | public int Maximum 172 | { 173 | get => _maximum; 174 | 175 | set 176 | { 177 | if (value < 0 || value > ushort.MaxValue) 178 | throw new ArgumentOutOfRangeException(nameof(value)); 179 | 180 | DenyIfBoundAndNotCreated(); 181 | DenyIfWaitingForInitialization(); 182 | 183 | // We only update the TaskDialog if the current state is a 184 | // non-marquee progress bar. 185 | if (BoundPage != null && !ProgressBarStateIsMarquee(_state)) 186 | { 187 | BoundPage.BoundTaskDialog.SetProgressBarRange( 188 | _minimum, 189 | value); 190 | } 191 | 192 | _maximum = value; 193 | } 194 | } 195 | 196 | /// 197 | /// 198 | /// 199 | /// 200 | /// This property can be set while the dialog is shown. 201 | /// 202 | public int Value 203 | { 204 | get => _value; 205 | 206 | set 207 | { 208 | if (value < 0 || value > ushort.MaxValue) 209 | throw new ArgumentOutOfRangeException(nameof(value)); 210 | 211 | DenyIfBoundAndNotCreated(); 212 | DenyIfWaitingForInitialization(); 213 | 214 | // We only update the TaskDialog if the current state is a 215 | // non-marquee progress bar. 216 | if (BoundPage != null && !ProgressBarStateIsMarquee(_state)) 217 | { 218 | BoundPage.BoundTaskDialog.SetProgressBarPosition( 219 | value); 220 | } 221 | 222 | _value = value; 223 | } 224 | } 225 | 226 | /// 227 | /// 228 | /// 229 | /// 230 | /// This property can be set while the dialog is shown. 231 | /// 232 | public int MarqueeSpeed 233 | { 234 | get => _marqueeSpeed; 235 | 236 | set 237 | { 238 | DenyIfBoundAndNotCreated(); 239 | 240 | int previousMarqueeSpeed = _marqueeSpeed; 241 | _marqueeSpeed = value; 242 | try 243 | { 244 | // We only update the TaskDialog if the current state is a 245 | // marquee progress bar. 246 | if (BoundPage != null && ProgressBarStateIsMarquee(_state)) 247 | State = _state; 248 | } 249 | catch 250 | { 251 | _marqueeSpeed = previousMarqueeSpeed; 252 | throw; 253 | } 254 | } 255 | } 256 | 257 | internal override bool IsCreatable 258 | { 259 | get => base.IsCreatable && _state != TaskDialogProgressBarState.None; 260 | } 261 | 262 | private static bool ProgressBarStateIsMarquee( 263 | TaskDialogProgressBarState state) 264 | { 265 | return state == TaskDialogProgressBarState.Marquee || 266 | state == TaskDialogProgressBarState.MarqueePaused; 267 | } 268 | 269 | private static int GetNativeProgressBarState( 270 | TaskDialogProgressBarState state) 271 | { 272 | switch (state) 273 | { 274 | case TaskDialogProgressBarState.Normal: 275 | return TaskDialogNativeMethods.PBST_NORMAL; 276 | case TaskDialogProgressBarState.Paused: 277 | return TaskDialogNativeMethods.PBST_PAUSED; 278 | case TaskDialogProgressBarState.Error: 279 | return TaskDialogNativeMethods.PBST_ERROR; 280 | default: 281 | throw new ArgumentException(); 282 | } 283 | } 284 | 285 | private protected override TaskDialogFlags BindCore() 286 | { 287 | TaskDialogFlags flags = base.BindCore(); 288 | 289 | if (ProgressBarStateIsMarquee(_state)) 290 | flags |= TaskDialogFlags.TDF_SHOW_MARQUEE_PROGRESS_BAR; 291 | else 292 | flags |= TaskDialogFlags.TDF_SHOW_PROGRESS_BAR; 293 | 294 | return flags; 295 | } 296 | 297 | private protected override void ApplyInitializationCore() 298 | { 299 | if (_state == TaskDialogProgressBarState.Marquee) 300 | { 301 | State = _state; 302 | } 303 | else if (_state != TaskDialogProgressBarState.MarqueePaused) 304 | { 305 | State = _state; 306 | BoundPage.BoundTaskDialog.SetProgressBarRange( 307 | _minimum, 308 | _maximum); 309 | BoundPage.BoundTaskDialog.SetProgressBarPosition( 310 | _value); 311 | 312 | // See comment in property "State" for why we need to set 313 | // the position it twice. 314 | if (_state != TaskDialogProgressBarState.Normal) 315 | BoundPage.BoundTaskDialog.SetProgressBarPosition( 316 | _value); 317 | } 318 | } 319 | } 320 | } 321 | -------------------------------------------------------------------------------- /TaskDialog/TaskDialogProgressBarState.cs: -------------------------------------------------------------------------------- 1 | namespace KPreisser.UI 2 | { 3 | /// 4 | /// 5 | /// 6 | public enum TaskDialogProgressBarState : int 7 | { 8 | /// 9 | /// Shows a regular progress bar. 10 | /// 11 | Normal = 0, 12 | 13 | /// 14 | /// Shows a paused (yellow) progress bar. 15 | /// 16 | Paused, 17 | 18 | /// 19 | /// Shows an error (red) progress bar. 20 | /// 21 | Error, 22 | 23 | /// 24 | /// Shows a marquee progress bar. 25 | /// 26 | Marquee, 27 | 28 | /// 29 | /// Shows a marquee progress bar where the marquee animation is paused. 30 | /// 31 | /// 32 | /// For example, if you switch from to 33 | /// while the dialog is shown, the 34 | /// marquee animation will stop. 35 | /// 36 | MarqueePaused, 37 | 38 | /// 39 | /// The progress bar will not be displayed. 40 | /// 41 | /// 42 | /// Note that while the dialog is showing, you cannot switch from 43 | /// to any other state, and vice versa. 44 | /// 45 | None 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /TaskDialog/TaskDialogRadioButton.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using TaskDialogFlags = KPreisser.UI.TaskDialogNativeMethods.TASKDIALOG_FLAGS; 4 | 5 | namespace KPreisser.UI 6 | { 7 | /// 8 | /// 9 | /// 10 | public sealed class TaskDialogRadioButton : TaskDialogControl 11 | { 12 | private string _text; 13 | 14 | private int _radioButtonID; 15 | 16 | private bool _enabled = true; 17 | 18 | private bool _checked; 19 | 20 | private TaskDialogRadioButtonCollection _collection; 21 | 22 | private bool _ignoreRadioButtonClickedNotification; 23 | 24 | /// 25 | /// Occurs when the value of the property has changed 26 | /// while this control is bound to a task dialog. 27 | /// 28 | public event EventHandler CheckedChanged; 29 | 30 | /// 31 | /// 32 | /// 33 | public TaskDialogRadioButton() 34 | : base() 35 | { 36 | } 37 | 38 | /// 39 | /// 40 | /// 41 | public TaskDialogRadioButton(string text) 42 | : this() 43 | { 44 | _text = text; 45 | } 46 | 47 | /// 48 | /// 49 | /// 50 | /// 51 | /// This property can be set while the dialog is shown. 52 | /// 53 | public bool Enabled 54 | { 55 | get => _enabled; 56 | 57 | set 58 | { 59 | DenyIfBoundAndNotCreated(); 60 | 61 | // Check if we can update the button. 62 | if (CanUpdate()) 63 | { 64 | BoundPage.BoundTaskDialog.SetRadioButtonEnabled( 65 | _radioButtonID, 66 | value); 67 | } 68 | 69 | _enabled = value; 70 | } 71 | } 72 | 73 | /// 74 | /// 75 | /// 76 | public string Text 77 | { 78 | get => _text; 79 | 80 | set 81 | { 82 | DenyIfBound(); 83 | 84 | _text = value; 85 | } 86 | } 87 | 88 | /// 89 | /// 90 | /// 91 | /// 92 | /// This property can be set to true while the dialog is shown (except 93 | /// from within the event). 94 | /// 95 | public bool Checked 96 | { 97 | get => _checked; 98 | 99 | set 100 | { 101 | DenyIfBoundAndNotCreated(); 102 | DenyIfWaitingForInitialization(); 103 | 104 | if (BoundPage == null) 105 | { 106 | _checked = value; 107 | 108 | // If we are part of a collection, set the checked value of all 109 | // all other buttons to False. 110 | // Note that this does not handle buttons that are added later 111 | // to the collection. 112 | if (_collection != null && value) 113 | { 114 | foreach (TaskDialogRadioButton radioButton in _collection) 115 | radioButton._checked = radioButton == this; 116 | } 117 | } 118 | else 119 | { 120 | // Unchecking a radio button is not possible in the task dialog. 121 | // TODO: Should we throw only if the new value is different than the 122 | // old one? 123 | if (!value) 124 | throw new InvalidOperationException( 125 | "Cannot uncheck a radio button while it is bound to a task dialog."); 126 | 127 | // Note: We do not allow to set the "Checked" property of any 128 | // radio button of the current task dialog while we are within 129 | // the TDN_RADIO_BUTTON_CLICKED notification handler. This is 130 | // because the logic of the task dialog is such that the radio 131 | // button will be selected AFTER the callback returns (not 132 | // before it is called), at least when the event is caused by 133 | // code sending the TDM_CLICK_RADIO_BUTTON message. This is 134 | // mentioned in the documentation for TDM_CLICK_RADIO_BUTTON: 135 | // "The specified radio button ID is sent to the 136 | // TaskDialogCallbackProc callback function as part of a 137 | // TDN_RADIO_BUTTON_CLICKED notification code. After the 138 | // callback function returns, the radio button will be 139 | // selected." 140 | // 141 | // While we handle this by ignoring the 142 | // TDN_RADIO_BUTTON_CLICKED notification when it is caused by 143 | // sending a TDM_CLICK_RADIO_BUTTON message, and then raising 144 | // the events after the notification handler returned, this 145 | // still seems to cause problems for TDN_RADIO_BUTTON_CLICKED 146 | // notifications initially caused by the user clicking the radio 147 | // button in the UI. 148 | // 149 | // For example, consider a scenario with two radio buttons 150 | // [ID 1 and 2], and the user added an event handler to 151 | // automatically select the first radio button (ID 1) when the 152 | // second one (ID 2) is selected in the UI. 153 | // This means the stack will then look as follows: 154 | // Show() -> 155 | // Callback: TDN_RADIO_BUTTON_CLICKED [ID 2] -> 156 | // SendMessage: TDM_CLICK_RADIO_BUTTON [ID 1] -> 157 | // Callback: TDN_RADIO_BUTTON_CLICKED [ID 1] 158 | // 159 | // However, when the initial TDN_RADIO_BUTTON_CLICKED handler 160 | // (ID 2) returns, the task dialog again calls the handler for 161 | // ID 1 (which wouldn't be a problem), and then again calls it 162 | // for ID 2, which is unexpected (and it doesn't seem that we 163 | // can prevent this by returning S_FALSE in the notification 164 | // handler). Additionally, after that it even seems we get an 165 | // endless loop of TDN_RADIO_BUTTON_CLICKED notifications even 166 | // when we don't send any further messages to the dialog. 167 | // See: 168 | // https://gist.github.com/kpreisser/c9d07225d801783c4b5fed0fac563469 169 | if (BoundPage.BoundTaskDialog.RadioButtonClickedStackCount > 0) 170 | throw new InvalidOperationException( 171 | $"Cannot set the " + 172 | $"{nameof(TaskDialogRadioButton)}.{nameof(Checked)} " + 173 | $"property from within the " + 174 | $"{nameof(TaskDialogRadioButton)}.{nameof(CheckedChanged)} " + 175 | $"event of one of the radio buttons of the current task dialog."); 176 | 177 | // Click the radio button which will (recursively) raise the 178 | // TDN_RADIO_BUTTON_CLICKED notification. However, we ignore 179 | // the notification and then raise the events afterwards - see 180 | // above. 181 | _ignoreRadioButtonClickedNotification = true; 182 | try 183 | { 184 | BoundPage.BoundTaskDialog.ClickRadioButton( 185 | _radioButtonID); 186 | } 187 | finally 188 | { 189 | _ignoreRadioButtonClickedNotification = false; 190 | } 191 | 192 | // Now raise the events. 193 | // Note: We also increment the stack count here to prevent 194 | // navigating the dialog and setting the Checked property 195 | // within the event handlers here even though this would work 196 | // correctly for the native API (as we are not in the 197 | // TDN_RADIO_BUTTON_CLICKED notification), because we are 198 | // raising two events (Unchecked+Checked), and when the 199 | // second event is called, the dialog might already be 200 | // navigated or another radio button may have been checked. 201 | TaskDialog boundTaskDialog = BoundPage.BoundTaskDialog; 202 | checked 203 | { 204 | boundTaskDialog.RadioButtonClickedStackCount++; 205 | } 206 | try 207 | { 208 | HandleRadioButtonClicked(); 209 | } 210 | finally 211 | { 212 | boundTaskDialog.RadioButtonClickedStackCount--; 213 | } 214 | } 215 | } 216 | } 217 | 218 | internal int RadioButtonID 219 | { 220 | get => _radioButtonID; 221 | } 222 | 223 | internal TaskDialogRadioButtonCollection Collection 224 | { 225 | get => _collection; 226 | set => _collection = value; 227 | } 228 | 229 | internal override bool IsCreatable 230 | { 231 | get => base.IsCreatable && !TaskDialogPage.IsNativeStringNullOrEmpty(_text); 232 | } 233 | 234 | /// 235 | /// 236 | /// 237 | /// 238 | public override string ToString() 239 | { 240 | return _text ?? base.ToString(); 241 | } 242 | 243 | internal TaskDialogFlags Bind(TaskDialogPage page, int radioButtonID) 244 | { 245 | TaskDialogFlags result = Bind(page); 246 | _radioButtonID = radioButtonID; 247 | 248 | return result; 249 | } 250 | 251 | internal void HandleRadioButtonClicked() 252 | { 253 | // Check if we need to ignore the notification when it is caused by 254 | // sending the TDM_CLICK_RADIO_BUTTON message. 255 | if (_ignoreRadioButtonClickedNotification) 256 | return; 257 | 258 | if (!_checked) 259 | { 260 | _checked = true; 261 | 262 | // Before raising the CheckedChanged event for the current button, 263 | // uncheck the other radio buttons and call their events (there 264 | // should be no more than one other button that is already checked). 265 | foreach (TaskDialogRadioButton radioButton in BoundPage.RadioButtons) 266 | { 267 | if (radioButton != this && radioButton._checked) 268 | { 269 | radioButton._checked = false; 270 | radioButton.OnCheckedChanged(EventArgs.Empty); 271 | } 272 | } 273 | 274 | // Finally, call the event for the current button. 275 | OnCheckedChanged(EventArgs.Empty); 276 | } 277 | } 278 | 279 | private protected override void UnbindCore() 280 | { 281 | _radioButtonID = 0; 282 | 283 | base.UnbindCore(); 284 | } 285 | 286 | private protected override void ApplyInitializationCore() 287 | { 288 | // Re-set the properties so they will make the necessary calls. 289 | if (!_enabled) 290 | Enabled = _enabled; 291 | } 292 | 293 | private bool CanUpdate() 294 | { 295 | // Only update the button when bound to a task dialog and we are not 296 | // waiting for the Navigated event. In the latter case we don't throw 297 | // an exception however, because ApplyInitialization() will be called 298 | // in the Navigated handler that does the necessary updates. 299 | return BoundPage?.WaitingForInitialization == false; 300 | } 301 | 302 | private void OnCheckedChanged(EventArgs e) 303 | { 304 | CheckedChanged?.Invoke(this, e); 305 | } 306 | } 307 | } 308 | -------------------------------------------------------------------------------- /TaskDialog/TaskDialogRadioButtonCollection.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.ObjectModel; 4 | 5 | namespace KPreisser.UI 6 | { 7 | /// 8 | /// 9 | /// 10 | public class TaskDialogRadioButtonCollection 11 | : Collection 12 | { 13 | // HashSet to detect duplicate items. 14 | private readonly HashSet _itemSet = 15 | new HashSet(); 16 | 17 | private TaskDialogPage _boundPage; 18 | 19 | /// 20 | /// 21 | /// 22 | public TaskDialogRadioButtonCollection() 23 | : base() 24 | { 25 | } 26 | 27 | internal TaskDialogPage BoundPage 28 | { 29 | get => _boundPage; 30 | set => _boundPage = value; 31 | } 32 | 33 | /// 34 | /// 35 | /// 36 | /// 37 | /// 38 | public TaskDialogRadioButton Add(string text) 39 | { 40 | var button = new TaskDialogRadioButton() 41 | { 42 | Text = text 43 | }; 44 | 45 | Add(button); 46 | return button; 47 | } 48 | 49 | /// 50 | /// 51 | /// 52 | /// 53 | /// 54 | protected override void SetItem(int index, TaskDialogRadioButton item) 55 | { 56 | // Disallow collection modification, so that we don't need to copy it 57 | // when binding the TaskDialogPage. 58 | _boundPage?.DenyIfBound(); 59 | DenyIfHasOtherCollection(item); 60 | 61 | TaskDialogRadioButton oldItem = this[index]; 62 | if (oldItem != item) 63 | { 64 | // First, add the new item (which will throw if it is a duplicate entry), 65 | // then remove the old one. 66 | if (!_itemSet.Add(item)) 67 | throw new ArgumentException(); 68 | _itemSet.Remove(oldItem); 69 | 70 | oldItem.Collection = null; 71 | item.Collection = this; 72 | } 73 | 74 | base.SetItem(index, item); 75 | } 76 | 77 | /// 78 | /// 79 | /// 80 | /// 81 | /// 82 | protected override void InsertItem(int index, TaskDialogRadioButton item) 83 | { 84 | // Disallow collection modification, so that we don't need to copy it 85 | // when binding the TaskDialogPage. 86 | _boundPage?.DenyIfBound(); 87 | DenyIfHasOtherCollection(item); 88 | 89 | if (!_itemSet.Add(item)) 90 | throw new ArgumentException(); 91 | 92 | item.Collection = this; 93 | base.InsertItem(index, item); 94 | } 95 | 96 | /// 97 | /// 98 | /// 99 | /// 100 | protected override void RemoveItem(int index) 101 | { 102 | // Disallow collection modification, so that we don't need to copy it 103 | // when binding the TaskDialogPage. 104 | _boundPage?.DenyIfBound(); 105 | 106 | TaskDialogRadioButton oldItem = this[index]; 107 | oldItem.Collection = null; 108 | _itemSet.Remove(oldItem); 109 | base.RemoveItem(index); 110 | } 111 | 112 | /// 113 | /// 114 | /// 115 | protected override void ClearItems() 116 | { 117 | // Disallow collection modification, so that we don't need to copy it 118 | // when binding the TaskDialogPage. 119 | _boundPage?.DenyIfBound(); 120 | 121 | foreach (TaskDialogRadioButton button in this) 122 | button.Collection = null; 123 | 124 | _itemSet.Clear(); 125 | base.ClearItems(); 126 | } 127 | 128 | private void DenyIfHasOtherCollection(TaskDialogRadioButton item) 129 | { 130 | if (item.Collection != null && item.Collection != this) 131 | throw new InvalidOperationException( 132 | "This control is already part of a different collection."); 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /TaskDialog/TaskDialogResult.cs: -------------------------------------------------------------------------------- 1 | namespace KPreisser.UI 2 | { 3 | /// 4 | /// 5 | /// 6 | public enum TaskDialogResult : int 7 | { 8 | /// 9 | /// 10 | /// 11 | None = 0, 12 | 13 | /// 14 | /// 15 | /// 16 | OK = TaskDialogNativeMethods.IDOK, 17 | 18 | /// 19 | /// 20 | /// 21 | /// 22 | /// Note: Adding a Cancel button will automatically add a close button 23 | /// to the task dialog's title bar and will allow to close the dialog by 24 | /// pressing ESC or Alt+F4 (just as if you enabled 25 | /// ). 26 | /// 27 | Cancel = TaskDialogNativeMethods.IDCANCEL, 28 | 29 | /// 30 | /// 31 | /// 32 | Abort = TaskDialogNativeMethods.IDABORT, 33 | 34 | /// 35 | /// 36 | /// 37 | Retry = TaskDialogNativeMethods.IDRETRY, 38 | 39 | /// 40 | /// 41 | /// 42 | Ignore = TaskDialogNativeMethods.IDIGNORE, 43 | 44 | /// 45 | /// 46 | /// 47 | Yes = TaskDialogNativeMethods.IDYES, 48 | 49 | /// 50 | /// 51 | /// 52 | No = TaskDialogNativeMethods.IDNO, 53 | 54 | /// 55 | /// 56 | /// 57 | Close = TaskDialogNativeMethods.IDCLOSE, 58 | 59 | /// 60 | /// 61 | /// 62 | /// 63 | /// Note: Clicking this button will not close the dialog, but will raise the 64 | /// event. 65 | /// 66 | Help = TaskDialogNativeMethods.IDHELP, 67 | 68 | /// 69 | /// 70 | /// 71 | TryAgain = TaskDialogNativeMethods.IDTRYAGAIN, 72 | 73 | /// 74 | /// 75 | /// 76 | Continue = TaskDialogNativeMethods.IDCONTINUE 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /TaskDialog/TaskDialogStandardButton.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace KPreisser.UI 4 | { 5 | /// 6 | /// 7 | /// 8 | public sealed class TaskDialogStandardButton : TaskDialogButton 9 | { 10 | private TaskDialogResult _result; 11 | 12 | private bool _visible = true; 13 | 14 | /// 15 | /// 16 | /// 17 | public TaskDialogStandardButton() 18 | // Use 'OK' by default instead of 'None' (which would not be a valid 19 | // standard button). 20 | : this(TaskDialogResult.OK) 21 | { 22 | } 23 | 24 | /// 25 | /// 26 | /// 27 | /// 28 | public TaskDialogStandardButton( 29 | TaskDialogResult result) 30 | : base() 31 | { 32 | if (!IsValidStandardButtonResult(result)) 33 | throw new ArgumentOutOfRangeException(nameof(result)); 34 | 35 | _result = result; 36 | } 37 | 38 | /// 39 | /// Gets or sets the which is represented by 40 | /// this . 41 | /// 42 | public TaskDialogResult Result 43 | { 44 | get => _result; 45 | 46 | set 47 | { 48 | if (!IsValidStandardButtonResult(value)) 49 | throw new ArgumentOutOfRangeException(nameof(value)); 50 | 51 | DenyIfBound(); 52 | 53 | // If we are part of a StandardButtonCollection, we must now notify it 54 | // that we changed our result. 55 | Collection?.HandleKeyChange( 56 | this, 57 | value); 58 | 59 | // If this was successful or we are not part of a collection, 60 | // we can now set the result. 61 | _result = value; 62 | } 63 | } 64 | 65 | /// 66 | /// Gets or sets a value that indicates if this 67 | /// should be shown when displaying 68 | /// the Task Dialog. 69 | /// 70 | /// 71 | /// Setting this to false allows you to still receive the 72 | /// event (e.g. for the 73 | /// button when 74 | /// is set), or to call the 75 | /// method even if the button 76 | /// is not shown. 77 | /// 78 | public bool Visible 79 | { 80 | get => _visible; 81 | 82 | set 83 | { 84 | DenyIfBound(); 85 | 86 | _visible = value; 87 | } 88 | } 89 | 90 | internal override bool IsCreatable 91 | { 92 | get => base.IsCreatable && _visible; 93 | } 94 | 95 | internal override int ButtonID 96 | { 97 | get => (int)_result; 98 | } 99 | 100 | internal new TaskDialogStandardButtonCollection Collection 101 | { 102 | get => (TaskDialogStandardButtonCollection)base.Collection; 103 | set => base.Collection = value; 104 | } 105 | 106 | private static TaskDialogButtons GetButtonFlagForResult( 107 | TaskDialogResult result) 108 | { 109 | switch (result) 110 | { 111 | case TaskDialogResult.OK: 112 | return TaskDialogButtons.OK; 113 | case TaskDialogResult.Cancel: 114 | return TaskDialogButtons.Cancel; 115 | case TaskDialogResult.Abort: 116 | return TaskDialogButtons.Abort; 117 | case TaskDialogResult.Retry: 118 | return TaskDialogButtons.Retry; 119 | case TaskDialogResult.Ignore: 120 | return TaskDialogButtons.Ignore; 121 | case TaskDialogResult.Yes: 122 | return TaskDialogButtons.Yes; 123 | case TaskDialogResult.No: 124 | return TaskDialogButtons.No; 125 | case TaskDialogResult.Close: 126 | return TaskDialogButtons.Close; 127 | case TaskDialogResult.Help: 128 | return TaskDialogButtons.Help; 129 | case TaskDialogResult.TryAgain: 130 | return TaskDialogButtons.TryAgain; 131 | case TaskDialogResult.Continue: 132 | return TaskDialogButtons.Continue; 133 | default: 134 | return default; 135 | } 136 | } 137 | 138 | private static bool IsValidStandardButtonResult( 139 | TaskDialogResult result) 140 | { 141 | return GetButtonFlagForResult(result) != default; 142 | } 143 | 144 | /// 145 | /// 146 | /// 147 | /// 148 | public override string ToString() 149 | { 150 | return _result.ToString(); 151 | } 152 | 153 | internal TaskDialogButtons GetButtonFlag() 154 | { 155 | return GetButtonFlagForResult(_result); 156 | } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /TaskDialog/TaskDialogStandardButtonCollection.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.ObjectModel; 4 | 5 | namespace KPreisser.UI 6 | { 7 | /// 8 | /// 9 | /// 10 | public class TaskDialogStandardButtonCollection 11 | : KeyedCollection 12 | { 13 | private TaskDialogPage _boundPage; 14 | 15 | /// 16 | /// 17 | /// 18 | public TaskDialogStandardButtonCollection() 19 | : base() 20 | { 21 | } 22 | 23 | /// 24 | /// 25 | /// 26 | /// 27 | public static implicit operator TaskDialogStandardButtonCollection( 28 | TaskDialogButtons buttons) 29 | { 30 | var collection = new TaskDialogStandardButtonCollection(); 31 | 32 | // Get the button results for the flags. 33 | foreach (TaskDialogResult result in GetResultsForButtonFlags(buttons)) 34 | collection.Add(new TaskDialogStandardButton(result)); 35 | 36 | return collection; 37 | } 38 | 39 | internal TaskDialogPage BoundPage 40 | { 41 | get => _boundPage; 42 | set => _boundPage = value; 43 | } 44 | 45 | /// 46 | /// 47 | /// 48 | /// 49 | internal static IEnumerable GetResultsForButtonFlags( 50 | TaskDialogButtons buttons) 51 | { 52 | // Note: The order in which we yield the results is the order in which 53 | // the task dialog actually displays the buttons. 54 | if ((buttons & TaskDialogButtons.OK) == TaskDialogButtons.OK) 55 | yield return TaskDialogResult.OK; 56 | if ((buttons & TaskDialogButtons.Yes) == TaskDialogButtons.Yes) 57 | yield return TaskDialogResult.Yes; 58 | if ((buttons & TaskDialogButtons.No) == TaskDialogButtons.No) 59 | yield return TaskDialogResult.No; 60 | if ((buttons & TaskDialogButtons.Abort) == TaskDialogButtons.Abort) 61 | yield return TaskDialogResult.Abort; 62 | if ((buttons & TaskDialogButtons.Retry) == TaskDialogButtons.Retry) 63 | yield return TaskDialogResult.Retry; 64 | if ((buttons & TaskDialogButtons.Cancel) == TaskDialogButtons.Cancel) 65 | yield return TaskDialogResult.Cancel; 66 | if ((buttons & TaskDialogButtons.Ignore) == TaskDialogButtons.Ignore) 67 | yield return TaskDialogResult.Ignore; 68 | if ((buttons & TaskDialogButtons.TryAgain) == TaskDialogButtons.TryAgain) 69 | yield return TaskDialogResult.TryAgain; 70 | if ((buttons & TaskDialogButtons.Continue) == TaskDialogButtons.Continue) 71 | yield return TaskDialogResult.Continue; 72 | if ((buttons & TaskDialogButtons.Close) == TaskDialogButtons.Close) 73 | yield return TaskDialogResult.Close; 74 | if ((buttons & TaskDialogButtons.Help) == TaskDialogButtons.Help) 75 | yield return TaskDialogResult.Help; 76 | } 77 | 78 | /// 79 | /// 80 | /// 81 | /// 82 | /// 83 | public TaskDialogStandardButton Add(TaskDialogResult result) 84 | { 85 | var button = new TaskDialogStandardButton(result); 86 | Add(button); 87 | 88 | return button; 89 | } 90 | 91 | internal void HandleKeyChange( 92 | TaskDialogStandardButton button, 93 | TaskDialogResult newKey) 94 | { 95 | ChangeItemKey(button, newKey); 96 | } 97 | 98 | /// 99 | /// 100 | /// 101 | /// 102 | /// 103 | protected override TaskDialogResult GetKeyForItem(TaskDialogStandardButton item) 104 | { 105 | return item.Result; 106 | } 107 | 108 | /// 109 | /// 110 | /// 111 | /// 112 | /// 113 | protected override void SetItem(int index, TaskDialogStandardButton item) 114 | { 115 | // Disallow collection modification, so that we don't need to copy it 116 | // when binding the TaskDialogPage. 117 | _boundPage?.DenyIfBound(); 118 | DenyIfHasOtherCollection(item); 119 | 120 | TaskDialogStandardButton oldItem = this[index]; 121 | 122 | // Call the base method first, as it will throw if we would insert a 123 | // duplicate item. 124 | base.SetItem(index, item); 125 | 126 | oldItem.Collection = null; 127 | item.Collection = this; 128 | } 129 | 130 | /// 131 | /// 132 | /// 133 | /// 134 | /// 135 | protected override void InsertItem(int index, TaskDialogStandardButton item) 136 | { 137 | // Disallow collection modification, so that we don't need to copy it 138 | // when binding the TaskDialogPage. 139 | _boundPage?.DenyIfBound(); 140 | DenyIfHasOtherCollection(item); 141 | 142 | // Call the base method first, as it will throw if we would insert a 143 | // duplicate item. 144 | base.InsertItem(index, item); 145 | 146 | item.Collection = this; 147 | } 148 | 149 | /// 150 | /// 151 | /// 152 | /// 153 | protected override void RemoveItem(int index) 154 | { 155 | // Disallow collection modification, so that we don't need to copy it 156 | // when binding the TaskDialogPage. 157 | _boundPage?.DenyIfBound(); 158 | 159 | TaskDialogStandardButton oldItem = this[index]; 160 | oldItem.Collection = null; 161 | base.RemoveItem(index); 162 | } 163 | 164 | /// 165 | /// 166 | /// 167 | protected override void ClearItems() 168 | { 169 | // Disallow collection modification, so that we don't need to copy it 170 | // when binding the TaskDialogPage. 171 | _boundPage?.DenyIfBound(); 172 | 173 | foreach (TaskDialogStandardButton button in this) 174 | button.Collection = null; 175 | base.ClearItems(); 176 | } 177 | 178 | private void DenyIfHasOtherCollection(TaskDialogStandardButton item) 179 | { 180 | if (item.Collection != null && item.Collection != this) 181 | throw new InvalidOperationException( 182 | "This control is already part of a different collection."); 183 | } 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /TaskDialog/TaskDialogStandardIcon.cs: -------------------------------------------------------------------------------- 1 | namespace KPreisser.UI 2 | { 3 | /// 4 | /// 5 | /// 6 | public enum TaskDialogStandardIcon : int 7 | { 8 | /// 9 | /// 10 | /// 11 | None = 0, 12 | 13 | /// 14 | /// 15 | /// 16 | Information = ushort.MaxValue - 2, // TD_INFORMATION_ICON 17 | 18 | /// 19 | /// 20 | /// 21 | Warning = ushort.MaxValue, // TD_WARNING_ICON 22 | 23 | /// 24 | /// 25 | /// 26 | Error = ushort.MaxValue - 1, // TD_ERROR_ICON 27 | 28 | /// 29 | /// 30 | /// 31 | SecurityShield = ushort.MaxValue - 3, // TD_SHIELD_ICON 32 | 33 | /// 34 | /// 35 | /// 36 | SecurityShieldBlueBar = ushort.MaxValue - 4, 37 | 38 | /// 39 | /// 40 | /// 41 | SecurityShieldGrayBar = ushort.MaxValue - 8, 42 | 43 | /// 44 | /// 45 | /// 46 | SecurityWarningYellowBar = ushort.MaxValue - 5, 47 | 48 | /// 49 | /// 50 | /// 51 | SecurityErrorRedBar = ushort.MaxValue - 6, 52 | 53 | /// 54 | /// 55 | /// 56 | SecuritySuccessGreenBar = ushort.MaxValue - 7, 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /TaskDialog/TaskDialogStandardIconContainer.cs: -------------------------------------------------------------------------------- 1 | namespace KPreisser.UI 2 | { 3 | internal class TaskDialogStandardIconContainer : TaskDialogIcon 4 | { 5 | public TaskDialogStandardIconContainer(TaskDialogStandardIcon icon) 6 | : base() 7 | { 8 | Icon = icon; 9 | } 10 | 11 | public TaskDialogStandardIcon Icon 12 | { 13 | get; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /TaskDialog/TaskDialogStartupLocation.cs: -------------------------------------------------------------------------------- 1 | namespace KPreisser.UI 2 | { 3 | /// 4 | /// 5 | /// 6 | public enum TaskDialogStartupLocation 7 | { 8 | /// 9 | /// 10 | /// 11 | CenterScreen = 0, 12 | 13 | /// 14 | /// 15 | /// 16 | CenterParent = 1 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /TaskDialog/WindowSubclassHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using System.Diagnostics; 4 | using System.Runtime.InteropServices; 5 | 6 | namespace KPreisser.UI 7 | { 8 | internal class WindowSubclassHandler : IDisposable 9 | { 10 | private readonly IntPtr _handle; 11 | 12 | private bool _opened; 13 | 14 | private bool _disposed; 15 | 16 | private IntPtr _originalWindowProc; 17 | 18 | /// 19 | /// The delegate for the callback handler (that calls 20 | /// from which the native function 21 | /// pointer is created. 22 | /// 23 | /// 24 | /// We must store this delegate (and prevent it from being garbage-collected) 25 | /// to ensure the function pointer doesn't become invalid. 26 | /// 27 | /// Note: We create a new delegate (and native function pointer) for each 28 | /// instance because even though creation will be slower (and requires a 29 | /// bit of memory to store the native code) it will be faster when the window 30 | /// procedure is invoked, because otherwise we would need to use a dictionary 31 | /// to map the hWnd to the instance, as the window procedure doesn't allow 32 | /// to store reference data. However, this is also the way that the 33 | /// NativeWindow class of WinForms does it. 34 | /// 35 | private readonly WindowSubclassHandlerNativeMethods.WindowProc _windowProcDelegate; 36 | 37 | /// 38 | /// The function pointer created from . 39 | /// 40 | private readonly IntPtr _windowProcDelegatePtr; 41 | 42 | public WindowSubclassHandler(IntPtr handle) 43 | { 44 | if (handle == IntPtr.Zero) 45 | throw new ArgumentNullException(nameof(handle)); 46 | 47 | _handle = handle; 48 | 49 | // Create a delegate for our window procedure, and get a function 50 | // pointer for it. 51 | _windowProcDelegate = (hWnd, msg, wParam, lParam) => 52 | { 53 | Debug.Assert(hWnd == _handle); 54 | return WndProc(msg, wParam, lParam); 55 | }; 56 | 57 | _windowProcDelegatePtr = Marshal.GetFunctionPointerForDelegate( 58 | _windowProcDelegate); 59 | } 60 | 61 | /// 62 | /// Subclasses the window. 63 | /// 64 | /// 65 | /// You must call to undo the subclassing before 66 | /// the window is destroyed. 67 | /// 68 | /// 69 | public void Open() 70 | { 71 | if (_disposed) 72 | throw new ObjectDisposedException(nameof(WindowSubclassHandler)); 73 | if (_opened) 74 | throw new InvalidOperationException(); 75 | 76 | // Replace the existing window procedure with our one 77 | // ("instance subclassing"). 78 | // We need to explicitely clear the last Win32 error and then retrieve 79 | // it, to check if the call succeeded. 80 | WindowSubclassHandlerNativeMethods.SetLastError(0); 81 | _originalWindowProc = WindowSubclassHandlerNativeMethods.SetWindowLongPtr( 82 | _handle, 83 | WindowSubclassHandlerNativeMethods.GWLP_WNDPROC, 84 | _windowProcDelegatePtr); 85 | if (_originalWindowProc == IntPtr.Zero && Marshal.GetLastWin32Error() != 0) 86 | throw new Win32Exception(); 87 | 88 | Debug.Assert(_originalWindowProc != _windowProcDelegatePtr); 89 | 90 | _opened = true; 91 | } 92 | 93 | public void Dispose() 94 | { 95 | Dispose(true); 96 | GC.SuppressFinalize(this); 97 | } 98 | 99 | public void KeepCallbackDelegateAlive() 100 | { 101 | GC.KeepAlive(_windowProcDelegate); 102 | } 103 | 104 | protected virtual void Dispose(bool disposing) 105 | { 106 | if (!_disposed) 107 | { 108 | // We cannot do anything from the finalizer thread since we have 109 | // resoures that must only be accessed from the GUI thread. 110 | if (disposing && _opened) 111 | { 112 | // Check if the current window procedure is the correct one. 113 | // We need to explicitely clear the last Win32 error and then 114 | // retrieve it, to check if the call succeeded. 115 | WindowSubclassHandlerNativeMethods.SetLastError(0); 116 | IntPtr currentWindowProcedure = WindowSubclassHandlerNativeMethods.GetWindowLongPtr( 117 | _handle, 118 | WindowSubclassHandlerNativeMethods.GWLP_WNDPROC); 119 | if (currentWindowProcedure == IntPtr.Zero && Marshal.GetLastWin32Error() != 0) 120 | throw new Win32Exception(); 121 | 122 | if (currentWindowProcedure != _windowProcDelegatePtr) 123 | throw new InvalidOperationException( 124 | "The current window procedure is not the expected one."); 125 | 126 | // Undo the subclassing by restoring the original window 127 | // procedure. 128 | WindowSubclassHandlerNativeMethods.SetLastError(0); 129 | if (WindowSubclassHandlerNativeMethods.SetWindowLongPtr( 130 | _handle, 131 | WindowSubclassHandlerNativeMethods.GWLP_WNDPROC, 132 | _originalWindowProc) == IntPtr.Zero && 133 | Marshal.GetLastWin32Error() != 0) 134 | throw new Win32Exception(); 135 | 136 | // Ensure to keep the delegate alive up to the point after we 137 | // have undone the subclassing. 138 | KeepCallbackDelegateAlive(); 139 | } 140 | 141 | _disposed = true; 142 | } 143 | } 144 | 145 | protected virtual IntPtr WndProc( 146 | int msg, 147 | IntPtr wParam, 148 | IntPtr lParam) 149 | { 150 | // Call the original window procedure to process the message. 151 | if (_originalWindowProc != IntPtr.Zero) 152 | { 153 | return WindowSubclassHandlerNativeMethods.CallWindowProc( 154 | _originalWindowProc, 155 | _handle, 156 | msg, 157 | wParam, 158 | lParam); 159 | } 160 | 161 | return IntPtr.Zero; 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /TaskDialog/WindowSubclassHandlerNativeMethods.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace KPreisser.UI 5 | { 6 | internal static class WindowSubclassHandlerNativeMethods 7 | { 8 | public const int GWLP_WNDPROC = -4; 9 | 10 | [UnmanagedFunctionPointer(CallingConvention.StdCall)] 11 | public delegate IntPtr WindowProc( 12 | IntPtr hWnd, 13 | int msg, 14 | IntPtr wParam, 15 | IntPtr lParam); 16 | 17 | [DllImport("kernel32", 18 | EntryPoint = "SetLastError", 19 | ExactSpelling = true)] 20 | public static extern void SetLastError(int dwErrCode); 21 | 22 | [DllImport("user32", 23 | EntryPoint = "CallWindowProcW", 24 | ExactSpelling = true)] 25 | public static extern IntPtr CallWindowProc( 26 | IntPtr lpPrevWndFunc, 27 | IntPtr hWnd, 28 | int msg, 29 | IntPtr wParam, 30 | IntPtr lParam); 31 | 32 | public static IntPtr GetWindowLongPtr(IntPtr hWnd, int nIndex) 33 | { 34 | if (IntPtr.Size == 4) 35 | return (IntPtr)GetWindowLong32(hWnd, nIndex); 36 | 37 | return GetWindowLongPtr64(hWnd, nIndex); 38 | } 39 | 40 | public static IntPtr SetWindowLongPtr(IntPtr hWnd, int nIndex, IntPtr dwNewLong) 41 | { 42 | if (IntPtr.Size == 4) 43 | return (IntPtr)SetWindowLong32(hWnd, nIndex, (int)dwNewLong); 44 | 45 | return SetWindowLongPtr64(hWnd, nIndex, dwNewLong); 46 | } 47 | 48 | [DllImport("user32", 49 | EntryPoint = "GetWindowLongW", 50 | ExactSpelling = true, 51 | SetLastError = true)] 52 | private static extern int GetWindowLong32(IntPtr hWnd, int nIndex); 53 | 54 | [DllImport("user32", 55 | EntryPoint = "GetWindowLongPtrW", 56 | ExactSpelling = true, 57 | SetLastError = true)] 58 | private static extern IntPtr GetWindowLongPtr64(IntPtr hWnd, int nIndex); 59 | 60 | [DllImport("user32", 61 | EntryPoint = "SetWindowLongW", 62 | ExactSpelling = true, 63 | SetLastError = true)] 64 | private static extern int SetWindowLong32(IntPtr hWnd, int nIndex, int dwNewLong); 65 | 66 | [DllImport("user32", 67 | EntryPoint = "SetWindowLongPtrW", 68 | ExactSpelling = true, 69 | SetLastError = true)] 70 | private static extern IntPtr SetWindowLongPtr64(IntPtr hWnd, int nIndex, IntPtr dwNewLong); 71 | } 72 | } 73 | --------------------------------------------------------------------------------