WPF Ctrl+C Hotkey Registration Overrides/Stops Typical Copy Functionality - wpf

I have tried looking into this for a while now and have not been able to come up with an answer that has worked. I am trying to register my WPF C# application to listen for Ctrl+C from anywhere while the application is running. The code below is part of my application and does trigger when the user presses Ctrl+C, however, the contents of the Clipboard are stale (the contents are from a previous copy before the application was running). The AnimateWindow function is being called so I know my hotkey registration is working.
So, my issue is that it appears registering for the hotkey below is overriding the copy functionality. I would like to keep this functionality intact as it is part of how my application works. I need to take what was copied and use that data for a database query and to fill a TextBox.
This is just a snippet of the entire project, please let me know if additional example code is needed to help answer. Thank you!
[DllImport("user32.dll")]
private static extern bool RegisterHotKey(IntPtr hWnd, int id, uint fsModifiers, uint vk);
[DllImport("user32.dll")]
private static extern bool UnregisterHotKey(IntPtr hWnd, int id);
// CTRL + C Hotkey
private const int COPY_ID = 9001;
private const uint MOD_CONTROL = 0x0002; // Modifier: Control
private const uint VK_C = 0x43; // 'C' key
/* Registering the hotkeys to be monitored */
protected override void OnSourceInitialized(EventArgs e)
{
base.OnSourceInitialized(e);
_windowHandle = new WindowInteropHelper(this).Handle;
_source = HwndSource.FromHwnd(_windowHandle);
_source.AddHook(HwndHook);
RegisterHotKey(_windowHandle, COPY_ID, MOD_CONTROL, VK_C); // 'C' key
}
/* The hook for pressing a hotkey */
private IntPtr HwndHook(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
const int WM_HOTKEY = 0x0312;
switch (msg)
{
case WM_HOTKEY:
switch (wParam.ToInt32())
{
case COPY_ID:
AnimateWindow();
break;
}
}
}

Related

How to get keyboard scancode of pressed Key?

I make an WPF app where hotkeys are chosen for their physical position on the keyboard, rather than for what letter they represent.
Because different users use different layout (qwerty, azerty, etc), my app must be smart enough to be layout-agnostic.
I designated QWERTY as the layout of reference.
So for example a French user uses AZERTY. Therefore what the Q key is in QWERTY is A in AZERTY.
So in order to be agnostic I imagine I should do the following operation:
// Represents, in QWERTY, the top-left letter key.
Key myKey = Key.Q;
// ScanCode represent the physical key on the keyboard.
var scanCode = GetScanCodeFromKey(myKey, new CultureInfo("en-US"));
// Would be "A" if the user uses AZERTY.
Key agnosticKey = getKeyFromScanCode(scanCode, CultureInfo.CurrentCulture);
This seems feasible, but I cannot find a function that does GetScanCodeFromKey and getKeyFromScanCode.
The only relevant post I've found is Is it possible to create a keyboard layout that is identical to the keyboard used?
but unfortunately it seems to be made for win32, and not WPF. MapVirtualKeyEx is not accessible from WPF.
I have to remark that writing high level code (a WPF appliacation) that has to depend on low level concepts (hardware/machine details) is never a good idea. High level applications should depend on high level abstractions (input) of the low level data output. You always want to add an extra abstraction layer/interface between high level and low level or software and hardware.
Custom mapping
You can of course do the mapping yourself. Using Win32 API allows you to get the current active input language and deal with the low level concept of a keyboard layout.
First chose the base layout of your application e.g. en-GB. Then create a lookup table based on the supported layout languages.
Each entry of this table is another table used as conversion table: from current layout to application's internal base layout.
To simplify lookup table creation you can create a minimal table by focusing on the supported keys only.
You can create the conversion table from a file(s) e.g XML or JSON to the lookup table. This provides simple extensibility to an already deployed application.
private Dictionary<int, Dictionary<Key, Key>> LanguageLayouts { get; set; }
public MainWindow()
{
InitializeComponent();
this.LanguageLayouts = new Dictionary<int, Dictionary<Key, Key>>
{
{
CultureInfo.GetCultureInfo("fr-FR").LCID, new Dictionary<Key, Key>
{
{Key.A, Key.Q},
{Key.Z, Key.W},
{Key.E, Key.E},
{Key.R, Key.R},
{Key.T, Key.T},
{Key.Y, Key.Y}
}
}
};
}
private void OnPreviewKeyUp(object sender, KeyEventArgs e)
{
int lcid = GetCurrentKeyboardLayout().LCID;
if (this.LanguageLayouts.TryGetValue(lcid, out Dictionary<Key, Key> currentLayoutMap))
{
if (currentLayoutMap.TryGetValue(e.Key, out Key convertedKey))
{
HandlePressedKey(convertedKey);
}
}
}
[DllImport("user32.dll")] static extern IntPtr GetForegroundWindow();
[DllImport("user32.dll")] static extern uint GetWindowThreadProcessId(IntPtr hwnd, IntPtr proccess);
[DllImport("user32.dll")] static extern IntPtr GetKeyboardLayout(uint thread);
public CultureInfo GetCurrentKeyboardLayout()
{
IntPtr foregroundWindow = GetForegroundWindow();
uint foregroundProcess = GetWindowThreadProcessId(foregroundWindow, IntPtr.Zero);
int keyboardLayout = GetKeyboardLayout(foregroundProcess).ToInt32() & 0xFFFF;
return new CultureInfo(keyboardLayout);
}
Since keyboard scancodes are a low-level concept (hardware <-> OS), you have to use Win32 API to hook onto the Windows message loop and filter keystroke messages.
Windows keyboard drivers usually use the scancode set 1 code table to convert scancodes to actual key values.
Windows API scancodes are decimal based i.e presented as integer, while codes in reference tables are documented hex based.
Scancode Based Solution #1: System.Windows.Interop.HwndSource (Recommended)
HwndSource is a WPF ready wrapper that provides access to the Win32 window handle.
Note that HwndSource implements IDisposable.
You can listen to the Windows message loop by registering a callback of type HwndSourceHook by calling HwndSource.AddHook.
Also don't forget to unregister the callback by calling HwndSource.RemoveHook.
To handle keystroke messages, you have to monitor the loop for WM_KEYUP and WM_KEYDOWN messages. See Keyboard Input Notifications for a list of keyboard input messages.
See System-Defined Messages for an overview of all system messages e.g., Clipborad messages.
private async void OnLoaded(object sender, RoutedEventArgs e)
{
var hwndSource = PresentationSource.FromVisual(this) as HwndSource;
if (hwndSource != null)
{
hwndSource.AddHook(MainWindow.OnWindowsMessageReceived);
}
// Define filter constants (see docs for message codes)
private const int WM_KEYDOWN = 0x0100;
private const int WM_KEYUP = 0x0101;
private static IntPtr OnWindowsMessageReceived(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
switch (msg)
{
// Equivalent of Keyboard.KeyDown event (or UIElement.KeyDown)
case MainWindow.WM_KEYDOWN:
{
// Parameter 'wParam' is the Virtual-Key code.
// Convert the Win32 Virtual-Key into WPF Key
// using 'System.Windows.Input.KeyInterop'.
// The Key is the result of the virtual code that is already translated to the current layout.
// While the scancode remains the same, the Key changes according to the keyboard layout.
Key key = KeyInterop.KeyFromVirtualKey(wParam.ToInt32());
// TODO::Handle Key
// Parameter 'lParam' bit 16 - 23 represents the decimal scancode.
// See scancode set 1 for key to code mapping (note: table uses hex codes!): https://www.win.tue.nl/~aeb/linux/kbd/scancodes-10.html).
// Use bit mask to get the scancode related bits (16 - 23).https://www.win.tue.nl/~aeb/linux/kbd/scancodes-10.html)
var scancode = (lParam.ToInt64() >> 16) & 0xFF;
//TODO::Handle scancode
break;
}
// Equivalent of Keyboard.KeyUp event (or UIElement.KeyUp)
case MainWindow.WM_KEYUP:
{
// Parameter 'wParam' is the Virtual-Key code.
// Convert the Win32 Virtual-Key into WPF Key
// using 'System.Windows.Input.KeyInterop'.
// The Key is the result of the virtual code that is already translated to the current layout.
// While the scancode remains the same, the Key changes according to the keyboard layout.
Key key = KeyInterop.KeyFromVirtualKey(wParam.ToInt32());
// TODO::Handle Key
// Parameter 'lParam' bit 16 - 23 represents the decimal scancode.
// See scancode set 1 for key to code mapping (note: table uses hex codes!): https://www.win.tue.nl/~aeb/linux/kbd/scancodes-10.html).
// Use bit mask to get the scancode related bits (16 - 23).
var scancode = (lParam.ToInt64() >> 16) & 0xFF;
//TODO::Handle scancode
break;
}
}
return IntPtr.Zero;
}
Scancode based solution #2: SetWindowsHookExA
private void Initialize()
{
// Keep a strong reference to the delegate.
// Otherwise it will get garbage collected (Win32 API won't keep a reference to the delegate).
this.CallbackDelegate = OnKeyPressed;
// Argument '2' (WH_KEYBOARD) defines keystroke message monitoring (message filter).
// Argument 'OnKeyPressed' defines the callback.
SetWindowsHookEx(2, this.CallbackDelegate, IntPtr.Zero, AppDomain.GetCurrentThreadId());
}
protected delegate IntPtr HookProc(int code, IntPtr wParam, IntPtr lParam);
private HookProc CallbackDelegate { get; set; }
[DllImport("user32.dll")]
protected static extern IntPtr SetWindowsHookEx(int code, HookProc func, IntPtr hInstance, int threadID);
[DllImport("user32.dll")]
static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);
private IntPtr OnKeyPressed(int code, IntPtr wParam, IntPtr lParam)
{
// Parameter 'wParam' is the Virtual-Key code.
// Convert the Win32 Virtual-Key into WPF Key
// using 'System.Windows.Input.KeyInterop'
Key key = KeyInterop.KeyFromVirtualKey(wParam.ToInt32());
// TODO::Handle Key
// Parameter 'lParam' bit 16 - 23 represents the decimal scancode.
// See scancode set 1 for key to code mapping (note: table uses hex codes!): https://www.win.tue.nl/~aeb/linux/kbd/scancodes-10.html).
// Use bit mask to get the scancode related bits (16 - 23).
var scancode = (lParam.ToInt64() >> 16) & 0xFF;
// TODO::Handle scancode
// Let other callbacks handle the message too
return CallNextHookEx(IntPtr.Zero, code, wParam, lParam);
}

user32.dll FindWindowEx, finding elements by classname on remote WPF window

I have a WPF application that is being started from a command-line application.
I am trying to do some simple automation (get/set text, click some buttons, etc). I cannot seem to find any of the child windows in WPF.
I have working models with WPF and UIA Framework, WinForms and WinAPI, but cannot seem to get WinAPI and WPF to play nicely.
I have used UISpy, WinSpy++, Winspector, the UIA Verify app to look at the controls, etc, but they do not seem to carry the same information for WPF as WinForms.
For example, in the WinForms app, I see a textbox with a ClassName of "WindowsForms10.EDIT.app.0.33c0d9d" when I look via the spy tools. The UIA Automation Verify app is the only one to acknowledge the element exists and reports "TextBox".
So, my question is how do I find the correct class name to pass or is there an easier route to find the child elements?
// does not work in wpf
IntPtr child = NativeMethods.FindWindowEx(parent, prevElement, "TextBox", null);
// works in winforms
IntPtr child = NativeMethods.FindWindowEx(parent, prevElement, "WindowsForms10.EDIT.app.0.33c0d9d", null);
and here is the user32.dll imports I am using:
public class NativeMethods
{
public const int WM_SETTEXT = 0x000C;
public const int WM_GETTEXT = 0x000D;
public const uint CB_SHOWDROPDOWN = 0x014F;
public const uint CB_SETCURSEL = 0x014E;
public const int BN_CLICKED = 245;
public const uint WM_SYSCOMMAND = 0x0112;
public const int SC_CLOSE = 0xF060;
[DllImport("user32.dll", SetLastError = true)]
public static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
[DllImport("user32.dll", SetLastError = true)]
public static extern IntPtr FindWindowEx(IntPtr parentHandle, IntPtr childAfter, string className, string windowTitle);
[DllImport("user32.dll", SetLastError = false)]
public static extern IntPtr GetDlgItem(IntPtr hDlg, int nIDDlgItem);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = false)]
public static extern IntPtr SendMessage(HandleRef hWnd, uint Msg, IntPtr wParam, string lParam);
[DllImport("user32.dll")]
public static extern int SendMessage(int hWnd, uint Msg, int wParam, int lParam);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = false)]
public static extern int SendMessage(int hWnd, int Msg, int wParam, StringBuilder lParam);
}
If you want to automate WPF, you must use the UI Automation, not the "old thing of the past" windows API :-).
There is a good introduction on UI automation here: Bugslayer: GUI Control to Major Tom
There is also an interesting open source project named "White" that leverages UI automation: White on codeplex. There are some samples in there if you want to dig in.

Open DateTimePicker C# control programmatically

How can I open the DateTimePicker C# control programmatically?
I want to show the Calendar in the Datetime Picker control by sending keys to the control.
Is there a way we can do that?
Try the following
//part of the usings
using System.Runtime.InteropServices;
//declares
[DllImport("user32.dll")]
private static extern bool PostMessage(
IntPtr hWnd, // handle to destination window
Int32 msg, // message
Int32 wParam, // first message parameter
Int32 lParam // second message parameter
);
const Int32 WM_LBUTTONDOWN = 0x0201;
//method to call dropdown
private void button1_Click(object sender, EventArgs e)
{
Int32 x = dateTimePicker1.Width - 10;
Int32 y = dateTimePicker1.Height / 2;
Int32 lParam = x + y * 0x00010000;
PostMessage(dateTimePicker1.Handle, WM_LBUTTONDOWN, 1,lParam);
}
On my system (Windows 7, .NET 35) the other solutions did not work. I found another solution on a MS discussion site that did work.
using System.Runtime.InteropServices;
public static class Extensions
{
[DllImport("user32.dll", SetLastError = true)]
private static extern int SendMessage(IntPtr hWnd, uint Msg, int wParam, int lParam);
private const uint WM_SYSKEYDOWN = 0x104;
public static void Open(this DateTimePicker obj)
{
SendMessage(obj.Handle, WM_SYSKEYDOWN, (int)Keys.Down, 0);
}
}
Source : http://social.msdn.microsoft.com/Forums/windows/en-US/f2f0b213-d57a-46de-b924-e21b7ac0882e/programmatically-open-the-calendar-of-the-datetimepicker-control?forum=winforms
Usage:
dateTimePicker1.Open();
Warnings. This will not work if the dateTimePicker1 is a Control on DataGridView (ie if you try to make a pop-up DatePicker on the DGV). It does work if the Control is added to the Form instead. What will happen is that the synthesized cursor "down" event will be swallowed by the DGV, and will move the current cell pointer down one instead of drop-drop the Calendar of the DTP.
Source: https://social.msdn.microsoft.com/Forums/windows/en-US/f2f0b213-d57a-46de-b924-e21b7ac0882e/programmatically-open-the-calendar-of-the-datetimepicker-control?forum=winforms
Refer answer by David M Morton https://social.msdn.microsoft.com/profile/david%20m%20morton/?ws=usercard-mini
//DateTimePicker dtpicker
dtpicker.Select();
SendKeys.Send("%{DOWN}");
"%{DOWN}" Key combination - Alt key(%) +Down arrow
code to programmatically trigger key down event for datetimepicker
(particularly the event of click on the dropdown arrow in a datetimepicker)
Credit goes to astander for providing the solution, which makes a very nice extension:
using System.Linq;
using System.Runtime.InteropServices;
public static class Extensions {
[DllImport("user32.dll", CharSet = CharSet.Auto)]
private static extern int PostMessage(IntPtr hwnd, Int32 wMsg, Int32 wParam, Int32 lParam);
public static void Open(this DateTimePicker obj) {
const int WM_LBUTTONDOWN = 0x0201;
int width = obj.Width - 10;
int height = obj.Height / 2;
int lParam = width + height * 0x00010000; // VooDoo to shift height
PostMessage(obj.Handle, WM_LBUTTONDOWN, 1, lParam);
}
}
Usage:
dateTimePicker1.Open();
This way, you can reuse your Extension anytime you'd like, over and over, in any form using any DateTimePicker control.
The accepted answer is mostly correct, however you should also use:
PostMessage(dateTimePicker1.Handle, WM_LBUTTONUP, 1,lParam);
After posting the WM_LBUTTONDOWN event.
Also, obviously WM_LBUTTONUP must be previously defined:
const Int32 WM_LBUTTONUP = 0x0202;
So my answer is:
using System.Runtime.InteropServices;
//declares
[DllImport("user32.dll")]
private static extern bool PostMessage(
IntPtr hWnd, // handle to destination window
Int32 msg, // message
Int32 wParam, // first message parameter
Int32 lParam // second message parameter
);
const Int32 WM_LBUTTONDOWN = 0x0201;
const Int32 WM_LBUTTONUP = 0x0202;
//method to call dropdown
private void button1_Click(object sender, EventArgs e)
{
Int32 x = dateTimePicker1.Width - 10;
Int32 y = dateTimePicker1.Height / 2;
Int32 lParam = x + y * 0x00010000;
PostMessage(dateTimePicker1.Handle, WM_LBUTTONDOWN, 1,lParam);
PostMessage(dateTimePicker1.Handle, WM_LBUTTONUP, 1,lParam);
}
This avoids Mark Lakata's bug in Windows 7 and/or .NET 3.5.
The reasoning is simple: the original code simulates a mouse button down event, but doesn't get the mouse button up again as we would when we click the button.
In that regard, you can try it out yourself: if you press the left mouse button to open a DateTimePicker and don't release the button, you also won't be able to use the control.
Edit: Adapting jp2code's answer:
using System.Runtime.InteropServices;
public static class Extensions {
[DllImport("user32.dll", CharSet = CharSet.Auto)]
private static extern int PostMessage(IntPtr hwnd, Int32 wMsg, Int32 wParam, Int32 lParam);
public static void Open(this DateTimePicker obj) {
const int WM_LBUTTONDOWN = 0x0201;
const int WM_LBUTTONUP = 0x0202;
int width = obj.Width - 10;
int height = obj.Height / 2;
int lParam = width + height * 0x00010000; // VooDoo to shift height
PostMessage(obj.Handle, WM_LBUTTONDOWN, 1, lParam);
PostMessage(obj.Handle, WM_LBUTTONUP, 1, lParam);
}
}
I liked some of the previous ideas and finished with this (tested) Mix:
public static class Extensions {
public static void Open(this DateTimePicker obj)
{
obj.Select();
SendKeys.Send("%{DOWN}");
}
}
Usage:
dateTimePicker1.Open();
dateTimePicker2.Open();

Subscribing to mouse events of all controls in form

How can i easily catch the "mouse down" events of all the controls in a form, without manually subscribing to each and every event? (C#)
Something like the "KeyPreview" feature, but for the mouse events.
I found this to be the best solution for my purposes.
Create a new class derived from IMessageFilter:
public class GlobalMouseHandler : IMessageFilter
{
private const int WM_LBUTTONDOWN = 0x201;
public bool PreFilterMessage(ref Message m)
{
if (m.Msg == WM_LBUTTONDOWN)
{
// do something
((YourMainForm)Form.ActiveForm).YourMainForm_Click(null, null);
}
return false;
}
}
Then in your main form add this to register the message filter:
GlobalMouseHandler globalClick = new GlobalMouseHandler();
Application.AddMessageFilter(globalClick);
And add this function to do whatever you have to, in your form:
public void YourMainForm_Click(object sender, EventArgs e)
{
// do anything here...
}
Solution 1
Subscribing to each and every event on every control within a form is certainly the most simplest approach to take, since you just use the code given by Ramesh.
However, another technique involves overriding the default windows message processing method ("WndProc") on the parent control - in this case, the form that contains all the controls.
This has a side effect that you won't be able to detect when the mouse cursor moves over controls contained inside another parent control.
For example, you won't be able to detect when the mouse cursor is over a TextBox that is contained inside a TabControl. This is because the TabControl will continue to process all mouse events.
Solution 2
The following solution will overcome all issues in attempting to detect which control the mouse cursor is over using a technique known as windows hooks.
Hooks essentially allow us to trap mouse and keyboard events even before they are dispatched to the window with focus.
Here's a sample:
public enum HookType : int
{
WH_JOURNALRECORD = 0,
WH_JOURNALPLAYBACK = 1,
WH_KEYBOARD = 2,
WH_GETMESSAGE = 3,
WH_CALLWNDPROC = 4,
WH_CBT = 5,
WH_SYSMSGFILTER = 6,
WH_MOUSE = 7,
WH_HARDWARE = 8,
WH_DEBUG = 9,
WH_SHELL = 10,
WH_FOREGROUNDIDLE = 11,
WH_CALLWNDPROCRET = 12,
WH_KEYBOARD_LL = 13,
WH_MOUSE_LL = 14
}
[StructLayout(LayoutKind.Sequential)]
public struct POINT
{
public int X;
public int Y;
}
[StructLayout(LayoutKind.Sequential)]
public struct MouseHookStruct
{
public POINT pt;
public int hwnd;
public int hitTestCode;
public int dwExtraInfo;
}
[DllImport("user32.dll", SetLastError = true)]
static extern int SetWindowsHookEx(HookType hook, HookProc callback, IntPtr hInstance, uint dwThreadId);
[DllImport("user32.dll", SetLastError= true)]
static extern int CallNextHookEx(int hook, int code, IntPtr wParam, IntPtr lParam);
[DllImport("kernel32.dll")]
static extern int GetLastError();
[DllImport("kernel32.dll")]
static extern int GetCurrentThreadId();
public delegate int HookProc(int nCode, IntPtr wParam, IntPtr lParam);
private static int hHook;
public Form1()
{
InitializeComponent();
hHook = SetWindowsHookEx(HookType.WH_MOUSE, MouseHookProc, IntPtr.Zero, (uint)GetCurrentThreadId());
if (hHook == 0)
MessageBox.Show("GetLastError: " + GetLastError());
}
private int MouseHookProc(int code, IntPtr wParam, IntPtr lParam)
{
//Marshall the data from the callback.
MouseHookStruct mouseInfo = (MouseHookStruct)Marshal.PtrToStructure(lParam, typeof(MouseHookStruct));
if (code < 0)
{
return CallNextHookEx(hHook, code, wParam, lParam);
}
else
{
//Create a string variable that shows the current mouse coordinates.
String strCaption = "x = " + mouseInfo.pt.X.ToString("d") +
" y = " + mouseInfo.pt.Y.ToString("d");
//You must get the active form because it is a static function.
Form tempForm = Form.ActiveForm;
Control c = Control.FromHandle((IntPtr)mouseInfo.hwnd);
if (c != null)
label1.Text = c.Name;
else
label1.Text = "Control not found";
//Set the caption of the form.
tempForm.Text = strCaption;
return CallNextHookEx(hHook, code, wParam, lParam);
}
}
Other conttrols in the form cannot listen to the Mouse event handlers of the form. Because each control got its own mouse event listners.
But You can subscribe each controls mouse events to the forms mouse events
this.MouseDown += new System.Windows.Forms.MouseEventHandler(this.Form1_MDown);
this.label1.MouseDown += new System.Windows.Forms.MouseEventHandler(this.Form1_MDown);
this.ListBox1.MouseDown += new System.Windows.Forms.MouseEventHandler(this.Form1_MDown);
this way you can have single handler for all the controls mouse events.

How can I invoke the dialog to set printer options manually?

I'm using WPF and need to let users set some print related options like printer and printer properties (e.g. papertray, landscape/portrait, duplex, etc). I'm aware of the PrintDialog class to get a PrintQueue and PrintTicket object. However I need to create I custom solution and can not show the PrintDialog.
I manage to get the available PrintQueue objects and let users select a printer. I'm struggling with the printer properties.
My question is: how can I show the dialog in which a user can set the printer properties for the selected PrintQueue (the dialog that is shown when a user clicks on the Properties button in the WPF PrintDialog).
The following code was found here (minus the Window_Loaded event). I tested it and it seems to work like a charm. Obviously you'll have to set the printer name in the PrinterSettings object before displaying the dialog.
Hope this works for you:
[DllImport("kernel32.dll")]
static extern IntPtr GlobalLock(IntPtr hMem);
[DllImport("kernel32.dll")]
static extern bool GlobalUnlock(IntPtr hMem);
[DllImport("kernel32.dll")]
static extern bool GlobalFree(IntPtr hMem);
[DllImport("winspool.Drv", EntryPoint = "DocumentPropertiesW", SetLastError = true, ExactSpelling = true, CallingConvention = CallingConvention.StdCall)]
static extern int DocumentProperties(IntPtr hwnd, IntPtr hPrinter, [MarshalAs(UnmanagedType.LPWStr)] string pDeviceName, IntPtr pDevModeOutput, IntPtr pDevModeInput, int fMode);
private const Int32 DM_OUT_BUFFER = 14;
public void OpenPrinterPropertiesDialog(PrinterSettings printerSettings, System.IntPtr pHandle) {
IntPtr hDevMode = printerSettings.GetHdevmode();
IntPtr pDevMode = GlobalLock(hDevMode);
Int32 fMode = 0;
int sizeNeeded = DocumentProperties(pHandle, IntPtr.Zero, printerSettings.PrinterName, pDevMode, pDevMode, fMode);
IntPtr devModeData = Marshal.AllocHGlobal(sizeNeeded);
fMode = DM_OUT_BUFFER;
DocumentProperties(pHandle, IntPtr.Zero, printerSettings.PrinterName, devModeData, pDevMode, fMode);
GlobalUnlock(hDevMode);
printerSettings.SetHdevmode(devModeData);
printerSettings.DefaultPageSettings.SetHdevmode(devModeData);
GlobalFree(hDevMode);
Marshal.FreeHGlobal(devModeData);
}
private void Window_Loaded(object sender, RoutedEventArgs e) {
OpenPrinterPropertiesDialog(new PrinterSettings(), new WindowInteropHelper(this).Handle);
}
If you target x86 compilation and run from a x64 machine, the code from Pwninstein will not work: when allocating devModeData, DocumentPropreties will always fail and returns a sizeNeeded of -1, with a LastError code 13.
To solve the problem, either make sure you target AnyCPU or just change the call to DocumentPropreties to the following:
int sizeNeeded = DocumentProperties(pHandle,
IntPtr.Zero,
printerSettings.PrinterName,
IntPtr.Zero, // This solves it
pDevMode,
fMode);
Using IntPtr.Zero instead of a proper pointer to a DevMode structure looks wrong, but that first call to DocumentProperties does not attempt to modify the memory at that position. The only data returned by the call is the memory size needed to store the device mode data that represents the internal parameters of the print driver.
Reference:
PInvoke.Net page on DocumentProperties

Resources