1083 lines
36 KiB
C#
1083 lines
36 KiB
C#
using System;
|
|
using System.CodeDom.Compiler;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Linq;
|
|
using System.Linq.Expressions;
|
|
using System.Runtime.InteropServices;
|
|
using System.Windows.Forms;
|
|
using JD.Kernel.Classes;
|
|
using JD.Kernel.StaticHelpers;
|
|
using JD.Kernel.Structs;
|
|
|
|
namespace JD.MessageService
|
|
{
|
|
/// <summary>
|
|
/// A wrapper around Vista's TaskDialog API
|
|
/// </summary>
|
|
internal static partial class TaskDialog
|
|
{
|
|
#region Constants
|
|
|
|
private static readonly Version RequiredOSVersion = new Version(6, 0, 5243);
|
|
|
|
#endregion
|
|
|
|
#region Properties
|
|
|
|
public static bool IsSupported
|
|
{
|
|
get
|
|
{
|
|
OperatingSystem os = Environment.OSVersion;
|
|
if (os.Platform != PlatformID.Win32NT)
|
|
{
|
|
return false;
|
|
}
|
|
return os.Version >= RequiredOSVersion;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Methods
|
|
|
|
public static MessageDialog.Button Show(IWin32Window owner, MessageDialog dialog)
|
|
{
|
|
return Show(owner == null ? IntPtr.Zero : owner.Handle, dialog);
|
|
}
|
|
|
|
public static MessageDialog.Button Show(IntPtr hwnd, MessageDialog dialog)
|
|
{
|
|
Throw.InvalidOperationIf(!IsSupported, "TaskDialog API is only supported on Windows Vista and later");
|
|
|
|
using (new EnableThemingInScope())
|
|
using (var instance = new Instance(hwnd, dialog))
|
|
{
|
|
return instance.Show();
|
|
}
|
|
}
|
|
|
|
private static TaskDialogIcon ConvertIcon(MessageDialogIcon icon)
|
|
{
|
|
switch (icon)
|
|
{
|
|
case MessageDialogIcon.None:
|
|
return TaskDialogIcon.None;
|
|
|
|
case MessageDialogIcon.Information:
|
|
return TaskDialogIcon.Information;
|
|
|
|
case MessageDialogIcon.Warning:
|
|
return TaskDialogIcon.Warning;
|
|
|
|
case MessageDialogIcon.Error:
|
|
return TaskDialogIcon.Error;
|
|
|
|
case MessageDialogIcon.Shield:
|
|
return TaskDialogIcon.Shield;
|
|
|
|
default:
|
|
throw new NotImplementedException();
|
|
}
|
|
}
|
|
|
|
private static TaskDialogFlags ConvertFlags(Flags<MessageDialogFlags> flags, uint numButtons)
|
|
{
|
|
TaskDialogFlags result = TaskDialogFlags.None;
|
|
|
|
if (flags.Contains(MessageDialogFlags.UseCommandLinks) && numButtons > 0)
|
|
{
|
|
result |= TaskDialogFlags.TDF_USE_COMMAND_LINKS;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private static TaskDialogCommonButtons ConvertCommonButtons(MessageDialog dialog)
|
|
{
|
|
TaskDialogCommonButtons result = TaskDialogCommonButtons.None;
|
|
|
|
foreach (var button in dialog.Buttons)
|
|
{
|
|
if (button.IsCommonButton)
|
|
{
|
|
// Id corresponds directly
|
|
result |= ConvertCommonButton((MessageDialogCommonButton)button.Id);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private static int LocateDefaultButton(MessageDialog dialog)
|
|
{
|
|
foreach (var button in dialog.Buttons)
|
|
{
|
|
if (button.IsDefault)
|
|
{
|
|
return ToTaskDialogButtonId(button);
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
private static int ToTaskDialogButtonId(MessageDialog.Button button)
|
|
{
|
|
if (button.IsCommonButton)
|
|
{
|
|
switch ((MessageDialogCommonButton)button.Id)
|
|
{
|
|
case MessageDialogCommonButton.Ok:
|
|
return 1;
|
|
case MessageDialogCommonButton.Cancel:
|
|
return 2;
|
|
case MessageDialogCommonButton.Retry:
|
|
return 4;
|
|
case MessageDialogCommonButton.Yes:
|
|
return 6;
|
|
case MessageDialogCommonButton.No:
|
|
return 7;
|
|
case MessageDialogCommonButton.Close:
|
|
return 8;
|
|
default:
|
|
throw new NotImplementedException();
|
|
}
|
|
}
|
|
|
|
return button.Id;
|
|
}
|
|
|
|
private static TaskDialogCommonButtons ConvertCommonButton(MessageDialogCommonButton button)
|
|
{
|
|
switch (button)
|
|
{
|
|
case MessageDialogCommonButton.Ok:
|
|
return TaskDialogCommonButtons.Ok;
|
|
|
|
case MessageDialogCommonButton.Yes:
|
|
return TaskDialogCommonButtons.Yes;
|
|
|
|
case MessageDialogCommonButton.No:
|
|
return TaskDialogCommonButtons.No;
|
|
|
|
case MessageDialogCommonButton.Retry:
|
|
return TaskDialogCommonButtons.Retry;
|
|
|
|
case MessageDialogCommonButton.Cancel:
|
|
return TaskDialogCommonButtons.Cancel;
|
|
|
|
case MessageDialogCommonButton.Close:
|
|
return TaskDialogCommonButtons.Cancel;
|
|
|
|
default:
|
|
throw new NotImplementedException();
|
|
}
|
|
}
|
|
|
|
private static void ConvertButtons(ref TaskDialogConfig config, MessageDialog dialog)
|
|
{
|
|
var buttons = dialog.Buttons.Where(b => !b.IsCommonButton).ToList();
|
|
|
|
if (buttons.Count > 0)
|
|
{
|
|
int elementSize = Marshal.SizeOf(typeof(TaskDialogButton));
|
|
config.pButtons = Marshal.AllocHGlobal(elementSize * buttons.Count);
|
|
config.cButtons = (uint)buttons.Count;
|
|
|
|
IntPtr currentPtr = config.pButtons;
|
|
foreach (var button in buttons)
|
|
{
|
|
var marshalButton = ConvertButton(button);
|
|
Marshal.StructureToPtr(marshalButton, currentPtr, false);
|
|
currentPtr = (IntPtr)(currentPtr.ToInt64() + elementSize);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static TaskDialogButton ConvertButton(MessageDialog.Button button)
|
|
{
|
|
var result = new TaskDialogButton
|
|
{
|
|
buttonId = button.Id,
|
|
buttonText = button.Text
|
|
};
|
|
|
|
if (!string.IsNullOrEmpty(button.Content))
|
|
{
|
|
result.buttonText += "\n" + button.Content;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private static int ConvertResult(int result)
|
|
{
|
|
if (result >= MessageDialog.CustomButtonStartId)
|
|
{
|
|
return result;
|
|
}
|
|
|
|
if (result == 8)
|
|
{
|
|
// Not part of DialogResult..
|
|
return (int)MessageDialogCommonButton.Close;
|
|
}
|
|
|
|
switch ((DialogResult)result)
|
|
{
|
|
case DialogResult.OK:
|
|
return (int)MessageDialogCommonButton.Ok;
|
|
|
|
case DialogResult.Yes:
|
|
return (int)MessageDialogCommonButton.Yes;
|
|
|
|
case DialogResult.No:
|
|
return (int)MessageDialogCommonButton.No;
|
|
|
|
case DialogResult.Retry:
|
|
return (int)MessageDialogCommonButton.Retry;
|
|
|
|
case DialogResult.Cancel:
|
|
return (int)MessageDialogCommonButton.Cancel;
|
|
|
|
default:
|
|
throw new NotImplementedException();
|
|
}
|
|
}
|
|
|
|
private static void CreateConfig(IntPtr hwnd, MessageDialog dialog, ref TaskDialogConfig config)
|
|
{
|
|
config.cbSize = (uint)Marshal.SizeOf(typeof(TaskDialogConfig));
|
|
config.hwndParent = hwnd;
|
|
config.dwCommonButtons = ConvertCommonButtons(dialog);
|
|
|
|
if (!string.IsNullOrEmpty(dialog.Caption))
|
|
{
|
|
config.pszWindowTitle = dialog.Caption;
|
|
}
|
|
|
|
config.MainIcon = new IconUnion((int)ConvertIcon(dialog.MainIcon));
|
|
|
|
if (!string.IsNullOrEmpty(dialog.MainMessage))
|
|
{
|
|
config.pszMainInstruction = dialog.MainMessage;
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(dialog.Content))
|
|
{
|
|
config.pszContent = dialog.Content;
|
|
}
|
|
|
|
ConvertButtons(ref config, dialog);
|
|
|
|
if (!string.IsNullOrEmpty(dialog.CheckBoxText))
|
|
{
|
|
config.pszVerificationText = dialog.CheckBoxText;
|
|
}
|
|
|
|
config.nDefaultButton = LocateDefaultButton(dialog);
|
|
config.dwFlags = ConvertFlags(dialog.Flags, config.cButtons);
|
|
|
|
if (dialog.EscapeKeyReturnsNull)
|
|
{
|
|
if (dialog.Buttons.FirstOrDefault(button => button.IsCommonButton && button.Id == (int)MessageDialogCommonButton.Cancel) != null)
|
|
{
|
|
throw new ApplicationException("The MessageDialog.EscapeKeyReturnsNull should never be true when a cancel button is available since the underlying API will necessarily return the cancel button when the user presses the escape key");
|
|
}
|
|
config.dwFlags |= TaskDialogFlags.TDF_ALLOW_DIALOG_CANCELLATION;
|
|
}
|
|
|
|
if (dialog.IsChecked)
|
|
{
|
|
config.dwFlags |= TaskDialogFlags.TDF_VERIFICATION_FLAG_CHECKED;
|
|
}
|
|
|
|
if (dialog.ProgressBarMaximum > 0)
|
|
{
|
|
config.dwFlags |= TaskDialogFlags.TDF_SHOW_PROGRESS_BAR;
|
|
}
|
|
|
|
if (dialog.ShouldTick)
|
|
{
|
|
config.dwFlags |= TaskDialogFlags.TDF_CALLBACK_TIMER;
|
|
}
|
|
|
|
if (dialog.PositionRelativeToParent)
|
|
{
|
|
config.dwFlags |= TaskDialogFlags.TDF_POSITION_RELATIVE_TO_WINDOW;
|
|
}
|
|
}
|
|
|
|
private static void DestroyConfig(ref TaskDialogConfig config)
|
|
{
|
|
if (config.pButtons != IntPtr.Zero)
|
|
{
|
|
IntPtr currentPtr = config.pButtons;
|
|
for (int i = 0; i < config.cButtons; i++)
|
|
{
|
|
Marshal.DestroyStructure(currentPtr, typeof(TaskDialogButton));
|
|
currentPtr = (IntPtr)(currentPtr.ToInt64() + Marshal.SizeOf(typeof(TaskDialogButton)));
|
|
}
|
|
Marshal.FreeHGlobal(config.pButtons);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Dll Interop
|
|
|
|
[DllImport("ComCtl32.dll", CharSet = CharSet.Auto, PreserveSig = false)]
|
|
private static extern void TaskDialogIndirect([In] ref TaskDialogConfig pTaskConfig, [Out] out int pnButton, [Out] out int pnRadioButton, [Out] out bool pfVerificationFlagChecked);
|
|
|
|
[DllImport("user32.dll")]
|
|
public static extern int SendMessage(IntPtr hWnd, uint Msg, IntPtr wparam, IntPtr lparam);
|
|
|
|
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto, Pack = 4)]
|
|
[GeneratedCode("PInvoke.net", "1.0")] // To shut up gendarme about large struct size
|
|
private struct TaskDialogConfig
|
|
{
|
|
/// <summary>
|
|
/// Size of the structure in bytes.
|
|
/// </summary>
|
|
public uint cbSize;
|
|
|
|
/// <summary>
|
|
/// Parent window handle.
|
|
/// </summary>
|
|
public IntPtr hwndParent;
|
|
|
|
/// <summary>
|
|
/// Module instance handle for resources.
|
|
/// </summary>
|
|
public IntPtr hInstance;
|
|
|
|
/// <summary>
|
|
/// Flags.
|
|
/// </summary>
|
|
public TaskDialogFlags dwFlags; // TASKDIALOG_FLAGS (TDF_XXX) flags
|
|
|
|
/// <summary>
|
|
/// Bit flags for commonly used buttons.
|
|
/// </summary>
|
|
public TaskDialogCommonButtons dwCommonButtons; // TASKDIALOG_COMMON_BUTTON (TDCBF_XXX) flags
|
|
|
|
/// <summary>
|
|
/// Window title.
|
|
/// </summary>
|
|
[MarshalAs(UnmanagedType.LPWStr)]
|
|
public string pszWindowTitle; // string or MAKEINTRESOURCE()
|
|
|
|
/// <summary>
|
|
/// The Main icon. Overloaded member. Can be string, a handle, a special value or a resource ID.
|
|
/// </summary>
|
|
public IconUnion MainIcon;
|
|
|
|
/// <summary>
|
|
/// Main Instruction.
|
|
/// </summary>
|
|
[MarshalAs(UnmanagedType.LPWStr)]
|
|
public string pszMainInstruction;
|
|
|
|
/// <summary>
|
|
/// Content.
|
|
/// </summary>
|
|
[MarshalAs(UnmanagedType.LPWStr)]
|
|
public string pszContent;
|
|
|
|
/// <summary>
|
|
/// Count of custom Buttons.
|
|
/// </summary>
|
|
public uint cButtons;
|
|
|
|
/// <summary>
|
|
/// Array of custom buttons.
|
|
/// </summary>
|
|
public IntPtr pButtons;
|
|
|
|
/// <summary>
|
|
/// ID of default button.
|
|
/// </summary>
|
|
public int nDefaultButton;
|
|
|
|
/// <summary>
|
|
/// Count of radio Buttons.
|
|
/// </summary>
|
|
public uint cRadioButtons;
|
|
|
|
/// <summary>
|
|
/// Array of radio buttons.
|
|
/// </summary>
|
|
public IntPtr pRadioButtons;
|
|
|
|
/// <summary>
|
|
/// ID of default radio button.
|
|
/// </summary>
|
|
public int nDefaultRadioButton;
|
|
|
|
/// <summary>
|
|
/// Text for verification check box. often "Don't ask be again".
|
|
/// </summary>
|
|
[MarshalAs(UnmanagedType.LPWStr)]
|
|
public string pszVerificationText;
|
|
|
|
/// <summary>
|
|
/// Expanded Information.
|
|
/// </summary>
|
|
[MarshalAs(UnmanagedType.LPWStr)]
|
|
public string pszExpandedInformation;
|
|
|
|
/// <summary>
|
|
/// Text for expanded control.
|
|
/// </summary>
|
|
[MarshalAs(UnmanagedType.LPWStr)]
|
|
public string pszExpandedControlText;
|
|
|
|
/// <summary>
|
|
/// Text for expanded control.
|
|
/// </summary>
|
|
[MarshalAs(UnmanagedType.LPWStr)]
|
|
public string pszCollapsedControlText;
|
|
|
|
/// <summary>
|
|
/// Icon for the footer. An overloaded member link MainIcon.
|
|
/// </summary>
|
|
public IconUnion FooterIcon;
|
|
|
|
/// <summary>
|
|
/// Footer Text.
|
|
/// </summary>
|
|
[MarshalAs(UnmanagedType.LPWStr)]
|
|
public string pszFooter;
|
|
|
|
/// <summary>
|
|
/// Function pointer for callback.
|
|
/// </summary>
|
|
public TaskDialogCallback pfCallback;
|
|
|
|
/// <summary>
|
|
/// Data that will be passed to the call back.
|
|
/// </summary>
|
|
public IntPtr lpCallbackData;
|
|
|
|
/// <summary>
|
|
/// Width of the Task Dialog's area in DLU's.
|
|
/// </summary>
|
|
public uint cxWidth; // width of the Task Dialog's client area in DLU's. If 0, Task Dialog will calculate the ideal width.
|
|
}
|
|
|
|
// Packing must be set to 4 to make this work on 64-bit platforms.
|
|
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto, Pack = 4)]
|
|
private struct TaskDialogButton
|
|
{
|
|
/// <summary>
|
|
/// The ID of the button. This value is returned by TaskDialog.Show when the button is clicked.
|
|
/// </summary>
|
|
public int buttonId;
|
|
|
|
/// <summary>
|
|
/// The string that appears on the button.
|
|
/// </summary>
|
|
[MarshalAs(UnmanagedType.LPWStr)]
|
|
public string buttonText;
|
|
}
|
|
|
|
[Flags]
|
|
private enum TaskDialogFlags
|
|
{
|
|
None = 0,
|
|
/// <summary>
|
|
/// Enable hyperlinks.
|
|
/// </summary>
|
|
TDF_ENABLE_HYPERLINKS = 0x0001,
|
|
|
|
/// <summary>
|
|
/// Use icon handle for main icon.
|
|
/// </summary>
|
|
TDF_USE_HICON_MAIN = 0x0002,
|
|
|
|
/// <summary>
|
|
/// Use icon handle for footer icon.
|
|
/// </summary>
|
|
TDF_USE_HICON_FOOTER = 0x0004,
|
|
|
|
/// <summary>
|
|
/// Allow dialog to be cancelled, even if there is no cancel button.
|
|
/// </summary>
|
|
TDF_ALLOW_DIALOG_CANCELLATION = 0x0008,
|
|
|
|
/// <summary>
|
|
/// Use command links rather than buttons.
|
|
/// </summary>
|
|
TDF_USE_COMMAND_LINKS = 0x0010,
|
|
|
|
/// <summary>
|
|
/// Use command links with no icons rather than buttons.
|
|
/// </summary>
|
|
TDF_USE_COMMAND_LINKS_NO_ICON = 0x0020,
|
|
|
|
/// <summary>
|
|
/// Show expanded info in the footer area.
|
|
/// </summary>
|
|
TDF_EXPAND_FOOTER_AREA = 0x0040,
|
|
|
|
/// <summary>
|
|
/// Expand by default.
|
|
/// </summary>
|
|
TDF_EXPANDED_BY_DEFAULT = 0x0080,
|
|
|
|
/// <summary>
|
|
/// Start with verification flag already checked.
|
|
/// </summary>
|
|
TDF_VERIFICATION_FLAG_CHECKED = 0x0100,
|
|
|
|
/// <summary>
|
|
/// Show a progress bar.
|
|
/// </summary>
|
|
TDF_SHOW_PROGRESS_BAR = 0x0200,
|
|
|
|
/// <summary>
|
|
/// Show a marquee progress bar.
|
|
/// </summary>
|
|
TDF_SHOW_MARQUEE_PROGRESS_BAR = 0x0400,
|
|
|
|
/// <summary>
|
|
/// Callback every 200 milliseconds.
|
|
/// </summary>
|
|
TDF_CALLBACK_TIMER = 0x0800,
|
|
|
|
/// <summary>
|
|
/// Center the dialog on the owner window rather than the monitor.
|
|
/// </summary>
|
|
TDF_POSITION_RELATIVE_TO_WINDOW = 0x1000,
|
|
|
|
/// <summary>
|
|
/// Right to Left Layout.
|
|
/// </summary>
|
|
TDF_RTL_LAYOUT = 0x2000,
|
|
|
|
/// <summary>
|
|
/// No default radio button.
|
|
/// </summary>
|
|
TDF_NO_DEFAULT_RADIO_BUTTON = 0x4000,
|
|
|
|
/// <summary>
|
|
/// Task Dialog can be minimized.
|
|
/// </summary>
|
|
TDF_CAN_BE_MINIMIZED = 0x8000
|
|
}
|
|
|
|
[Flags]
|
|
private enum TaskDialogCommonButtons
|
|
{
|
|
/// <summary>
|
|
/// No common buttons.
|
|
/// </summary>
|
|
None = 0,
|
|
|
|
/// <summary>
|
|
/// OK common button. If selected Task Dialog will return DialogResult.OK.
|
|
/// </summary>
|
|
Ok = 0x0001,
|
|
|
|
/// <summary>
|
|
/// Yes common button. If selected Task Dialog will return DialogResult.Yes.
|
|
/// </summary>
|
|
Yes = 0x0002,
|
|
|
|
/// <summary>
|
|
/// No common button. If selected Task Dialog will return DialogResult.No.
|
|
/// </summary>
|
|
No = 0x0004,
|
|
|
|
/// <summary>
|
|
/// Cancel common button. If selected Task Dialog will return DialogResult.Cancel.
|
|
/// If this button is specified, the dialog box will respond to typical cancel actions (Alt-F4 and Escape).
|
|
/// </summary>
|
|
Cancel = 0x0008,
|
|
|
|
/// <summary>
|
|
/// Retry common button. If selected Task Dialog will return DialogResult.Retry.
|
|
/// </summary>
|
|
Retry = 0x0010,
|
|
|
|
/// <summary>
|
|
/// Close common button. If selected Task Dialog will return this value.
|
|
/// </summary>
|
|
Close = 0x0020,
|
|
}
|
|
|
|
private enum TaskDialogIcon
|
|
{
|
|
/// <summary>
|
|
/// No Icon.
|
|
/// </summary>
|
|
None = 0,
|
|
|
|
/// <summary>
|
|
/// System warning icon.
|
|
/// </summary>
|
|
Warning = 0xFFFF, // MAKEINTRESOURCEW(-1)
|
|
|
|
/// <summary>
|
|
/// System Error icon.
|
|
/// </summary>
|
|
Error = 0xFFFE, // MAKEINTRESOURCEW(-2)
|
|
|
|
/// <summary>
|
|
/// System Information icon.
|
|
/// </summary>
|
|
Information = 0xFFFD, // MAKEINTRESOURCEW(-3)
|
|
|
|
/// <summary>
|
|
/// Shield icon.
|
|
/// </summary>
|
|
Shield = 0xFFFC, // MAKEINTRESOURCEW(-4)
|
|
}
|
|
|
|
// NOTE: We include a "spacer" so that the struct size varies on 64-bit architectures.
|
|
[StructLayout(LayoutKind.Explicit, CharSet = CharSet.Auto)]
|
|
private struct IconUnion
|
|
{
|
|
internal IconUnion(int i)
|
|
{
|
|
spacer = IntPtr.Zero;
|
|
mainIcon = i;
|
|
}
|
|
|
|
[FieldOffset(0)]
|
|
private int mainIcon;
|
|
|
|
// This field is used to adjust the length of the structure on 32/64bit OS.
|
|
[FieldOffset(0)]
|
|
private IntPtr spacer;
|
|
}
|
|
|
|
private delegate int TaskDialogCallback([In] IntPtr hwnd, [In] uint msg, [In] UIntPtr wParam, [In] IntPtr lParam, [In] IntPtr refData);
|
|
|
|
/// <summary>Notifications returned by Task Dialogs to the callback.</summary>
|
|
/// <remarks>From CommCtrl.h.</remarks>
|
|
private enum TaskDialogNotification : uint
|
|
{
|
|
TDN_CREATED = 0,
|
|
TDN_NAVIGATED = 1,
|
|
TDN_BUTTON_CLICKED = 2, // wParam = Button ID
|
|
TDN_HYPERLINK_CLICKED = 3, // lParam = (LPCWSTR)pszHREF
|
|
TDN_TIMER = 4, // wParam = Milliseconds since dialog created or timer reset
|
|
TDN_DESTROYED = 5,
|
|
TDN_RADIO_BUTTON_CLICKED = 6, // wParam = Radio Button ID
|
|
TDN_DIALOG_CONSTRUCTED = 7,
|
|
TDN_VERIFICATION_CLICKED = 8, // wParam = 1 if checkbox checked, 0 if not, lParam is unused and always 0
|
|
TDN_HELP = 9,
|
|
TDN_EXPANDO_BUTTON_CLICKED = 10 // wParam = 0 (dialog is now collapsed), wParam != 0 (dialog is now expanded)
|
|
}
|
|
|
|
/// <summary>Messages that can be sent to Task Dialogs.</summary>
|
|
/// <remarks>From CommCtrl.h.</remarks>
|
|
private enum TaskDialogMessages : uint
|
|
{
|
|
TDM_NAVIGATE_PAGE = 0x0400 + 101,
|
|
TDM_CLICK_BUTTON = 0x0400 + 102, // wParam = Button ID
|
|
TDM_SET_MARQUEE_PROGRESS_BAR = 0x0400 + 103, // wParam = 0 (nonMarque) wParam != 0 (Marquee)
|
|
TDM_SET_PROGRESS_BAR_STATE = 0x0400 + 104, // wParam = new progress state
|
|
TDM_SET_PROGRESS_BAR_RANGE = 0x0400 + 105, // lParam = MAKELPARAM(nMinRange, nMaxRange)
|
|
TDM_SET_PROGRESS_BAR_POS = 0x0400 + 106, // wParam = new position
|
|
TDM_SET_PROGRESS_BAR_MARQUEE = 0x0400 + 107, // wParam = 0 (stop marquee), wParam != 0 (start marquee), lparam = speed (milliseconds between repaints)
|
|
TDM_SET_ELEMENT_TEXT = 0x0400 + 108, // wParam = element (TASKDIALOG_ELEMENTS), lParam = new element text (LPCWSTR)
|
|
TDM_CLICK_RADIO_BUTTON = 0x0400 + 110, // wParam = Radio Button ID
|
|
TDM_ENABLE_BUTTON = 0x0400 + 111, // lParam = 0 (disable), lParam != 0 (enable), wParam = Button ID
|
|
TDM_ENABLE_RADIO_BUTTON = 0x0400 + 112, // lParam = 0 (disable), lParam != 0 (enable), wParam = Radio Button ID
|
|
TDM_CLICK_VERIFICATION = 0x0400 + 113, // wParam = 0 (unchecked), 1 (checked), lParam = 1 (set key focus)
|
|
TDM_UPDATE_ELEMENT_TEXT = 0x0400 + 114, // wParam = element (TASKDIALOG_ELEMENTS), lParam = new element text (LPCWSTR)
|
|
TDM_SET_BUTTON_ELEVATION_REQUIRED_STATE = 0x0400 + 115, // wParam = Button ID, lParam = 0 (elevation not required), lParam != 0 (elevation required)
|
|
TDM_UPDATE_ICON = 0x0400 + 116 // wParam = icon element (TASKDIALOG_ICON_ELEMENTS), lParam = new icon (hIcon if TDF_USE_HICON_* was set, PCWSTR otherwise)
|
|
}
|
|
|
|
private enum ProgressBarState : uint
|
|
{
|
|
PBST_NORMAL = 1,
|
|
PBST_ERROR = 2,
|
|
PBST_PAUSE = 3
|
|
}
|
|
|
|
private enum TaskDialogElement : uint
|
|
{
|
|
TDE_CONTENT,
|
|
TDE_EXPANDED_INFORMATION,
|
|
TDE_FOOTER,
|
|
TDE_MAIN_INSTRUCTION
|
|
}
|
|
|
|
#endregion
|
|
|
|
|
|
private class InstanceData : IDisposable
|
|
{
|
|
#region Variables
|
|
|
|
private readonly Instance m_owner;
|
|
private readonly IntPtr m_hwndParent;
|
|
private readonly MessageDialog m_dialog;
|
|
|
|
private TaskDialogConfig m_config;
|
|
private IntPtr m_customConfigPtr;
|
|
|
|
#endregion
|
|
|
|
#region Constructor
|
|
|
|
public InstanceData(Instance owner, IntPtr hwndParent, MessageDialog dialog)
|
|
{
|
|
m_owner = owner;
|
|
m_hwndParent = hwndParent;
|
|
m_dialog = dialog;
|
|
|
|
m_config = new TaskDialogConfig();
|
|
CreateConfig(hwndParent, dialog, ref m_config);
|
|
m_config.pfCallback = owner.Callback;
|
|
|
|
m_dialog.PropertyChanged += m_owner.WhenDialogPropertyChanged;
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
Dispose(true);
|
|
GC.SuppressFinalize(this);
|
|
}
|
|
|
|
void Dispose(bool disposing)
|
|
{
|
|
if (disposing)
|
|
{
|
|
m_dialog.PropertyChanged -= m_owner.WhenDialogPropertyChanged;
|
|
}
|
|
|
|
if (m_customConfigPtr != IntPtr.Zero)
|
|
{
|
|
Marshal.FreeHGlobal(m_customConfigPtr);
|
|
m_customConfigPtr = IntPtr.Zero;
|
|
}
|
|
|
|
DestroyConfig(ref m_config);
|
|
}
|
|
|
|
~InstanceData()
|
|
{
|
|
Dispose(false);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Properties
|
|
|
|
public MessageDialog Dialog
|
|
{
|
|
get { return m_dialog; }
|
|
}
|
|
|
|
public IntPtr HwndParent
|
|
{
|
|
get { return m_hwndParent; }
|
|
}
|
|
|
|
public TaskDialogConfig Configuration
|
|
{
|
|
get { return m_config; }
|
|
}
|
|
|
|
public IntPtr ConfigurationPtr
|
|
{
|
|
get
|
|
{
|
|
if (m_customConfigPtr == IntPtr.Zero)
|
|
{
|
|
m_customConfigPtr = Marshal.AllocHGlobal(Marshal.SizeOf(m_config));
|
|
Marshal.StructureToPtr(m_config, m_customConfigPtr, false);
|
|
}
|
|
|
|
return m_customConfigPtr;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
|
|
private class Instance : IDisposable
|
|
{
|
|
#region Variables
|
|
|
|
private static readonly PropertyChangedHandlers ms_propertyChangedHandlers = new PropertyChangedHandlers();
|
|
|
|
private readonly ElementStrings m_elementStrings = new ElementStrings();
|
|
|
|
private InstanceData m_data;
|
|
private IntPtr m_hwnd;
|
|
|
|
#endregion
|
|
|
|
#region Constructor
|
|
|
|
static Instance()
|
|
{
|
|
ms_propertyChangedHandlers.Register(d => d.MainMessage, i => i.WhenMainMessageChanged);
|
|
ms_propertyChangedHandlers.Register(d => d.Content, i => i.WhenContentChanged);
|
|
}
|
|
|
|
public Instance(IntPtr hwndParent, MessageDialog dialog)
|
|
{
|
|
m_data = new InstanceData(this, hwndParent, dialog);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
Dispose(true);
|
|
GC.SuppressFinalize(this);
|
|
}
|
|
|
|
private void Dispose(bool disposing)
|
|
{
|
|
if (disposing)
|
|
{
|
|
DisposableHelper.SafeDispose(ref m_data);
|
|
}
|
|
m_elementStrings.Clear();
|
|
}
|
|
|
|
~Instance()
|
|
{
|
|
Dispose(false);
|
|
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Properties
|
|
|
|
private MessageDialog Dialog
|
|
{
|
|
get { return m_data.Dialog; }
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Methods
|
|
|
|
public MessageDialog.Button Show()
|
|
{
|
|
var configuration = m_data.Configuration;
|
|
|
|
bool checkedResult;
|
|
int buttonIdResult;
|
|
int radioButtonIdResult;
|
|
|
|
TaskDialogIndirect(ref configuration, out buttonIdResult, out radioButtonIdResult, out checkedResult);
|
|
|
|
buttonIdResult = ConvertResult(buttonIdResult);
|
|
m_data.Dialog.IsChecked = checkedResult;
|
|
|
|
// The following expression will be unable to locate the button and will return null only in this very specific case:
|
|
// - No common button with MessageDialogCommonButton.Cancel has been specifically created in the dialog AND
|
|
// - The developer set MessageDialog.EscapeKeyReturnsNull on the MessageDialog AND
|
|
// - The user closed the dialog with the escape key
|
|
return m_data.Dialog.Buttons.FirstOrDefault(b => b.Id == buttonIdResult);
|
|
}
|
|
|
|
internal int Callback(IntPtr hwnd, uint msg, UIntPtr wParam, IntPtr lParam, IntPtr refData)
|
|
{
|
|
m_hwnd = hwnd;
|
|
|
|
switch ((TaskDialogNotification)msg)
|
|
{
|
|
case TaskDialogNotification.TDN_CREATED:
|
|
OnCreated(hwnd);
|
|
break;
|
|
|
|
case TaskDialogNotification.TDN_NAVIGATED:
|
|
OnCreated(hwnd);
|
|
CenterWindow.Center(hwnd);
|
|
break;
|
|
|
|
case TaskDialogNotification.TDN_TIMER:
|
|
OnTimer(hwnd, TimeSpan.FromMilliseconds(wParam.ToUInt64()));
|
|
return 1; // Reset ms count
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
private void OnCreated(IntPtr hwnd)
|
|
{
|
|
// Disable buttons that asked for it
|
|
foreach (var button in Dialog.Buttons)
|
|
{
|
|
if (!button.Enabled)
|
|
{
|
|
SendMessageHelper(hwnd, TaskDialogMessages.TDM_ENABLE_BUTTON, ToTaskDialogButtonId(button), 0);
|
|
}
|
|
}
|
|
|
|
// Set progress bar maximum
|
|
if (Dialog.ProgressBarMaximum > 0)
|
|
{
|
|
var progressBarRange = MakeLParam(0, Dialog.ProgressBarMaximum);
|
|
SendMessageHelper(hwnd, TaskDialogMessages.TDM_SET_PROGRESS_BAR_RANGE, 0, progressBarRange);
|
|
SendMessageHelper(hwnd, TaskDialogMessages.TDM_SET_PROGRESS_BAR_STATE, (int)ProgressBarState.PBST_NORMAL, 0);
|
|
}
|
|
}
|
|
|
|
private void OnTimer(IntPtr hwnd, TimeSpan deltaTime)
|
|
{
|
|
MessageDialog.TickEventArgs tickEventArgs = new MessageDialog.TickEventArgs(deltaTime);
|
|
Dialog.OnTick(tickEventArgs);
|
|
|
|
if (Dialog.ProgressBarMaximum > 0 && tickEventArgs.CurrentProgress >= 0)
|
|
{
|
|
SendMessageHelper(hwnd, TaskDialogMessages.TDM_SET_PROGRESS_BAR_POS, tickEventArgs.CurrentProgress, 0);
|
|
}
|
|
|
|
if (tickEventArgs.NewDialog != null)
|
|
{
|
|
NavigateTo(hwnd, tickEventArgs.NewDialog);
|
|
}
|
|
}
|
|
|
|
private static long MakeLParam(int wLow, int wHigh)
|
|
{
|
|
return (((short)wHigh << 16) | (wLow & 0xffff));
|
|
}
|
|
|
|
private static void SendMessageHelper(IntPtr hwnd, TaskDialogMessages message, int wParam, long lParam)
|
|
{
|
|
SendMessage(hwnd, (uint)message, (IntPtr)wParam, new IntPtr(lParam));
|
|
}
|
|
|
|
internal void WhenDialogPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
|
|
{
|
|
Debug.Assert(m_hwnd != IntPtr.Zero);
|
|
ms_propertyChangedHandlers.Handle(e.PropertyName, this, m_hwnd);
|
|
}
|
|
|
|
private void SetText(IntPtr hwnd, TaskDialogElement element, string text)
|
|
{
|
|
IntPtr stringPtr = m_elementStrings.Update(element, text);
|
|
SendMessage(hwnd, (uint)TaskDialogMessages.TDM_SET_ELEMENT_TEXT, (IntPtr)element, stringPtr);
|
|
}
|
|
|
|
private void WhenMainMessageChanged(IntPtr hwnd)
|
|
{
|
|
SetText(hwnd, TaskDialogElement.TDE_MAIN_INSTRUCTION, Dialog.MainMessage);
|
|
}
|
|
|
|
private void WhenContentChanged(IntPtr hwnd)
|
|
{
|
|
SetText(hwnd, TaskDialogElement.TDE_CONTENT, Dialog.Content);
|
|
}
|
|
|
|
private void NavigateTo(IntPtr hwnd, MessageDialog newDialog)
|
|
{
|
|
m_hwnd = IntPtr.Zero;
|
|
IntPtr hwndParent = m_data.HwndParent;
|
|
DisposableHelper.SafeDispose(m_data);
|
|
m_data = new InstanceData(this, hwndParent, newDialog);
|
|
SendMessage(hwnd, (uint)TaskDialogMessages.TDM_NAVIGATE_PAGE, IntPtr.Zero, m_data.ConfigurationPtr);
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
|
|
private class PropertyChangedHandlers
|
|
{
|
|
private readonly Dictionary<string, Func<Instance, Action<IntPtr>>> m_propertyChangedHandlers = new Dictionary<string, Func<Instance, Action<IntPtr>>>();
|
|
|
|
public void Register(Expression<Func<MessageDialog, object>> expression, Func<Instance, Action<IntPtr>> action)
|
|
{
|
|
m_propertyChangedHandlers.Add(MemberNameResolver.ResolveMemberName(expression), action);
|
|
}
|
|
|
|
public void Handle(string name, Instance instance, IntPtr hwnd)
|
|
{
|
|
Func<Instance, Action<IntPtr>> action;
|
|
if (m_propertyChangedHandlers.TryGetValue(name, out action))
|
|
{
|
|
action(instance)(hwnd);
|
|
}
|
|
}
|
|
}
|
|
|
|
private class ElementStrings
|
|
{
|
|
private readonly IntPtr[] m_elementStrings = new IntPtr[Enum.GetNames(typeof(TaskDialogElement)).Length];
|
|
|
|
public IntPtr Update(TaskDialogElement element, string text)
|
|
{
|
|
int elementIndex = (int)element;
|
|
Delete(elementIndex);
|
|
|
|
IntPtr newStringPtr = Marshal.StringToHGlobalUni(text);
|
|
m_elementStrings[elementIndex] = newStringPtr;
|
|
return newStringPtr;
|
|
}
|
|
|
|
public void Clear()
|
|
{
|
|
for (int i = 0; i < m_elementStrings.Length; i++)
|
|
{
|
|
Delete(i);
|
|
}
|
|
}
|
|
|
|
private void Delete(int i)
|
|
{
|
|
if (m_elementStrings[i] != IntPtr.Zero)
|
|
{
|
|
Marshal.FreeHGlobal(m_elementStrings[i]);
|
|
m_elementStrings[i] = IntPtr.Zero;
|
|
}
|
|
}
|
|
}
|
|
|
|
private static class CenterWindow
|
|
{
|
|
public static void Center(IntPtr hwnd)
|
|
{
|
|
Rect rect;
|
|
GetClientRect(hwnd, out rect);
|
|
|
|
var screen = Screen.FromHandle(hwnd);
|
|
int x = screen.WorkingArea.Width / 2 - rect.Width / 2;
|
|
int y = screen.WorkingArea.Height / 2 - rect.Height / 2;
|
|
|
|
SetWindowPos(hwnd, IntPtr.Zero, x, y, -1, -1, SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE);
|
|
}
|
|
|
|
private struct Rect
|
|
{
|
|
public int left;
|
|
public int top;
|
|
public int right;
|
|
public int bottom;
|
|
|
|
public int Width
|
|
{
|
|
get { return right - left; }
|
|
}
|
|
|
|
public int Height
|
|
{
|
|
get { return bottom - top; }
|
|
}
|
|
}
|
|
|
|
private const int SWP_NOACTIVATE = 0x0010;
|
|
private const int SWP_NOSIZE = 0x0001;
|
|
private const int SWP_NOZORDER = 0x0004;
|
|
|
|
[DllImport("user32.dll")]
|
|
private static extern bool GetClientRect(IntPtr hWnd, out Rect lpRect);
|
|
|
|
[DllImport("user32.dll")]
|
|
private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int x, int y, int cx, int cy, uint uFlags);
|
|
}
|
|
}
|
|
}
|