├── .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 |  
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 |
--------------------------------------------------------------------------------