645 lines
24 KiB
C#
645 lines
24 KiB
C#
// Copyright (c) Microsoft Corporation.
|
|
// Licensed under the MIT License.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.ObjectModel;
|
|
using System.Globalization;
|
|
using System.Management.Automation;
|
|
using System.Reflection;
|
|
using System.Runtime.InteropServices;
|
|
using System.Text;
|
|
using System.Threading;
|
|
|
|
using Microsoft.PowerShell.Commands.ShowCommandExtension;
|
|
|
|
namespace Microsoft.PowerShell.Commands
|
|
{
|
|
/// <summary>
|
|
/// Show-Command displays a GUI for a cmdlet, or for all cmdlets if no specific cmdlet is specified.
|
|
/// </summary>
|
|
[Cmdlet(VerbsCommon.Show, "Command", HelpUri = "https://go.microsoft.com/fwlink/?LinkID=2109589")]
|
|
public class ShowCommandCommand : PSCmdlet, IDisposable
|
|
{
|
|
#region Private Fields
|
|
/// <summary>
|
|
/// Set to true when ProcessRecord is reached, since it will always open a window.
|
|
/// </summary>
|
|
private bool _hasOpenedWindow;
|
|
|
|
/// <summary>
|
|
/// Determines if the command should be sent to the pipeline as a string instead of run.
|
|
/// </summary>
|
|
private bool _passThrough;
|
|
|
|
/// <summary>
|
|
/// Uses ShowCommandProxy to invoke WPF GUI object.
|
|
/// </summary>
|
|
private ShowCommandProxy _showCommandProxy;
|
|
|
|
/// <summary>
|
|
/// Data container for all cmdlets. This is populated when show-command is called with no command name.
|
|
/// </summary>
|
|
private List<ShowCommandCommandInfo> _commands;
|
|
|
|
/// <summary>
|
|
/// List of modules that have been loaded indexed by module name.
|
|
/// </summary>
|
|
private Dictionary<string, ShowCommandModuleInfo> _importedModules;
|
|
|
|
/// <summary>
|
|
/// Record the EndProcessing error.
|
|
/// </summary>
|
|
private PSDataCollection<ErrorRecord> _errors = new();
|
|
|
|
/// <summary>
|
|
/// Field used for the NoCommonParameter parameter.
|
|
/// </summary>
|
|
private SwitchParameter _noCommonParameter;
|
|
|
|
/// <summary>
|
|
/// Object used for ShowCommand with a command name that holds the view model created for the command.
|
|
/// </summary>
|
|
private object _commandViewModelObj;
|
|
#endregion
|
|
|
|
#region Input Cmdlet Parameter
|
|
/// <summary>
|
|
/// Gets or sets the command name.
|
|
/// </summary>
|
|
[Parameter(Position = 0)]
|
|
[Alias("CommandName")]
|
|
public string Name { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the Width.
|
|
/// </summary>
|
|
[Parameter]
|
|
[ValidateRange(300, int.MaxValue)]
|
|
public double Height { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the Width.
|
|
/// </summary>
|
|
[Parameter]
|
|
[ValidateRange(300, int.MaxValue)]
|
|
public double Width { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets a value indicating Common Parameters should not be displayed.
|
|
/// </summary>
|
|
[Parameter]
|
|
public SwitchParameter NoCommonParameter
|
|
{
|
|
get { return _noCommonParameter; }
|
|
|
|
set { _noCommonParameter = value; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets a value indicating errors should not cause a message window to be displayed.
|
|
/// </summary>
|
|
[Parameter]
|
|
public SwitchParameter ErrorPopup { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets a value indicating the command should be sent to the pipeline as a string instead of run.
|
|
/// </summary>
|
|
[Parameter]
|
|
public SwitchParameter PassThru
|
|
{
|
|
get { return _passThrough; }
|
|
|
|
set { _passThrough = value; }
|
|
}
|
|
#endregion
|
|
|
|
#region Public and Protected Methods
|
|
/// <summary>
|
|
/// Executes a PowerShell script, writing the output objects to the pipeline.
|
|
/// </summary>
|
|
/// <param name="script">Script to execute.</param>
|
|
public void RunScript(string script)
|
|
{
|
|
if (_showCommandProxy == null || string.IsNullOrEmpty(script))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (_passThrough)
|
|
{
|
|
this.WriteObject(script);
|
|
return;
|
|
}
|
|
|
|
if (ErrorPopup)
|
|
{
|
|
this.RunScriptSilentlyAndWithErrorHookup(script);
|
|
return;
|
|
}
|
|
|
|
if (_showCommandProxy.HasHostWindow)
|
|
{
|
|
if (!_showCommandProxy.SetPendingISECommand(script))
|
|
{
|
|
this.RunScriptSilentlyAndWithErrorHookup(script);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// Don't send newline at end as PSReadLine shows it rather than executing
|
|
if (!ConsoleInputWithNativeMethods.AddToConsoleInputBuffer(script, newLine: false))
|
|
{
|
|
this.WriteDebug(FormatAndOut_out_gridview.CannotWriteToConsoleInputBuffer);
|
|
this.RunScriptSilentlyAndWithErrorHookup(script);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Dispose method in IDisposable.
|
|
/// </summary>
|
|
public void Dispose()
|
|
{
|
|
this.Dispose(true);
|
|
GC.SuppressFinalize(this);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initialize a proxy instance for show-command.
|
|
/// </summary>
|
|
protected override void BeginProcessing()
|
|
{
|
|
_showCommandProxy = new ShowCommandProxy(this);
|
|
|
|
if (_showCommandProxy.ScreenHeight < this.Height)
|
|
{
|
|
ErrorRecord error = new(
|
|
new NotSupportedException(string.Format(CultureInfo.CurrentUICulture, FormatAndOut_out_gridview.PropertyValidate, "Height", _showCommandProxy.ScreenHeight)),
|
|
"PARAMETER_DATA_ERROR",
|
|
ErrorCategory.InvalidData,
|
|
null);
|
|
this.ThrowTerminatingError(error);
|
|
}
|
|
|
|
if (_showCommandProxy.ScreenWidth < this.Width)
|
|
{
|
|
ErrorRecord error = new(
|
|
new NotSupportedException(string.Format(CultureInfo.CurrentUICulture, FormatAndOut_out_gridview.PropertyValidate, "Width", _showCommandProxy.ScreenWidth)),
|
|
"PARAMETER_DATA_ERROR",
|
|
ErrorCategory.InvalidData,
|
|
null);
|
|
this.ThrowTerminatingError(error);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// ProcessRecord with or without CommandName.
|
|
/// </summary>
|
|
protected override void ProcessRecord()
|
|
{
|
|
if (Name == null)
|
|
{
|
|
_hasOpenedWindow = this.CanProcessRecordForAllCommands();
|
|
}
|
|
else
|
|
{
|
|
_hasOpenedWindow = this.CanProcessRecordForOneCommand();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Optionally displays errors in a message.
|
|
/// </summary>
|
|
protected override void EndProcessing()
|
|
{
|
|
if (!_hasOpenedWindow)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// We wait until the window is loaded and then activate it
|
|
// to work around the console window gaining activation somewhere
|
|
// in the end of ProcessRecord, which causes the keyboard focus
|
|
// (and use oif tab key to focus controls) to go away from the window
|
|
_showCommandProxy.WindowLoaded.WaitOne();
|
|
_showCommandProxy.ActivateWindow();
|
|
|
|
this.WaitForWindowClosedOrHelpNeeded();
|
|
this.RunScript(_showCommandProxy.GetScript());
|
|
|
|
if (_errors.Count == 0 || !ErrorPopup)
|
|
{
|
|
return;
|
|
}
|
|
|
|
StringBuilder errorString = new();
|
|
|
|
for (int i = 0; i < _errors.Count; i++)
|
|
{
|
|
if (i != 0)
|
|
{
|
|
errorString.AppendLine();
|
|
}
|
|
|
|
ErrorRecord error = _errors[i];
|
|
errorString.Append(error.Exception.Message);
|
|
}
|
|
|
|
_showCommandProxy.ShowErrorString(errorString.ToString());
|
|
}
|
|
|
|
/// <summary>
|
|
/// StopProcessing is called close the window when user press Ctrl+C in the command prompt.
|
|
/// </summary>
|
|
protected override void StopProcessing()
|
|
{
|
|
_showCommandProxy.CloseWindow();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Private Methods
|
|
/// <summary>
|
|
/// Runs the script in a new PowerShell instance and hooks up error stream to potentially display error popup.
|
|
/// This method has the inconvenience of not showing to the console user the script being executed.
|
|
/// </summary>
|
|
/// <param name="script">Script to be run.</param>
|
|
private void RunScriptSilentlyAndWithErrorHookup(string script)
|
|
{
|
|
// errors are not created here, because there is a field for it used in the final pop up
|
|
PSDataCollection<object> output = new();
|
|
|
|
output.DataAdded += this.Output_DataAdded;
|
|
_errors.DataAdded += this.Error_DataAdded;
|
|
|
|
System.Management.Automation.PowerShell ps = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace);
|
|
ps.Streams.Error = _errors;
|
|
|
|
ps.Commands.AddScript(script);
|
|
|
|
ps.Invoke(null, output, null);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Issues an error when this.commandName was not found.
|
|
/// </summary>
|
|
private void IssueErrorForNoCommand()
|
|
{
|
|
InvalidOperationException errorException = new(
|
|
string.Format(
|
|
CultureInfo.CurrentUICulture,
|
|
FormatAndOut_out_gridview.CommandNotFound,
|
|
Name));
|
|
this.ThrowTerminatingError(new ErrorRecord(errorException, "NoCommand", ErrorCategory.InvalidOperation, Name));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Issues an error when there is more than one command matching this.commandName.
|
|
/// </summary>
|
|
private void IssueErrorForMoreThanOneCommand()
|
|
{
|
|
InvalidOperationException errorException = new(
|
|
string.Format(
|
|
CultureInfo.CurrentUICulture,
|
|
FormatAndOut_out_gridview.MoreThanOneCommand,
|
|
Name,
|
|
"Show-Command"));
|
|
this.ThrowTerminatingError(new ErrorRecord(errorException, "MoreThanOneCommand", ErrorCategory.InvalidOperation, Name));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called from CommandProcessRecord to run the command that will get the CommandInfo and list of modules.
|
|
/// </summary>
|
|
/// <param name="command">Command to be retrieved.</param>
|
|
/// <param name="modules">List of loaded modules.</param>
|
|
private void GetCommandInfoAndModules(out CommandInfo command, out Dictionary<string, ShowCommandModuleInfo> modules)
|
|
{
|
|
command = null;
|
|
modules = null;
|
|
string commandText = _showCommandProxy.GetShowCommandCommand(Name, true);
|
|
|
|
Collection<PSObject> commandResults = this.InvokeCommand.InvokeScript(commandText);
|
|
|
|
object[] commandObjects = (object[])commandResults[0].BaseObject;
|
|
object[] moduleObjects = (object[])commandResults[1].BaseObject;
|
|
if (commandResults == null || moduleObjects == null || commandObjects.Length == 0)
|
|
{
|
|
this.IssueErrorForNoCommand();
|
|
return;
|
|
}
|
|
|
|
if (commandObjects.Length > 1)
|
|
{
|
|
this.IssueErrorForMoreThanOneCommand();
|
|
}
|
|
|
|
command = ((PSObject)commandObjects[0]).BaseObject as CommandInfo;
|
|
if (command == null)
|
|
{
|
|
this.IssueErrorForNoCommand();
|
|
return;
|
|
}
|
|
|
|
if (command.CommandType == CommandTypes.Alias)
|
|
{
|
|
commandText = _showCommandProxy.GetShowCommandCommand(command.Definition, false);
|
|
commandResults = this.InvokeCommand.InvokeScript(commandText);
|
|
if (commandResults == null || commandResults.Count != 1)
|
|
{
|
|
this.IssueErrorForNoCommand();
|
|
return;
|
|
}
|
|
|
|
command = (CommandInfo)commandResults[0].BaseObject;
|
|
}
|
|
|
|
modules = _showCommandProxy.GetImportedModulesDictionary(moduleObjects);
|
|
}
|
|
|
|
/// <summary>
|
|
/// ProcessRecord when a command name is specified.
|
|
/// </summary>
|
|
/// <returns>True if there was no exception processing this record.</returns>
|
|
private bool CanProcessRecordForOneCommand()
|
|
{
|
|
CommandInfo commandInfo;
|
|
this.GetCommandInfoAndModules(out commandInfo, out _importedModules);
|
|
Diagnostics.Assert(commandInfo != null, "GetCommandInfoAndModules would throw a terminating error/exception");
|
|
|
|
try
|
|
{
|
|
_commandViewModelObj = _showCommandProxy.GetCommandViewModel(new ShowCommandCommandInfo(commandInfo), _noCommonParameter.ToBool(), _importedModules, this.Name.Contains('\\'));
|
|
_showCommandProxy.ShowCommandWindow(_commandViewModelObj, _passThrough);
|
|
}
|
|
catch (TargetInvocationException ti)
|
|
{
|
|
this.WriteError(new ErrorRecord(ti.InnerException, "CannotProcessRecordForOneCommand", ErrorCategory.InvalidOperation, Name));
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// ProcessRecord when a command name is not specified.
|
|
/// </summary>
|
|
/// <returns>True if there was no exception processing this record.</returns>
|
|
private bool CanProcessRecordForAllCommands()
|
|
{
|
|
Collection<PSObject> rawCommands = this.InvokeCommand.InvokeScript(_showCommandProxy.GetShowAllModulesCommand());
|
|
|
|
_commands = _showCommandProxy.GetCommandList((object[])rawCommands[0].BaseObject);
|
|
_importedModules = _showCommandProxy.GetImportedModulesDictionary((object[])rawCommands[1].BaseObject);
|
|
|
|
try
|
|
{
|
|
_showCommandProxy.ShowAllModulesWindow(_importedModules, _commands, _noCommonParameter.ToBool(), _passThrough);
|
|
}
|
|
catch (TargetInvocationException ti)
|
|
{
|
|
this.WriteError(new ErrorRecord(ti.InnerException, "CannotProcessRecordForAllCommands", ErrorCategory.InvalidOperation, Name));
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Waits until the window has been closed answering HelpNeeded events.
|
|
/// </summary>
|
|
private void WaitForWindowClosedOrHelpNeeded()
|
|
{
|
|
while (true)
|
|
{
|
|
int which = WaitHandle.WaitAny(new WaitHandle[] { _showCommandProxy.WindowClosed, _showCommandProxy.HelpNeeded, _showCommandProxy.ImportModuleNeeded });
|
|
|
|
if (which == 0)
|
|
{
|
|
break;
|
|
}
|
|
|
|
if (which == 1)
|
|
{
|
|
Collection<PSObject> helpResults = this.InvokeCommand.InvokeScript(_showCommandProxy.GetHelpCommand(_showCommandProxy.CommandNeedingHelp));
|
|
_showCommandProxy.DisplayHelp(helpResults);
|
|
continue;
|
|
}
|
|
|
|
Diagnostics.Assert(which == 2, "which is 0,1 or 2 and 0 and 1 have been eliminated in the ifs above");
|
|
string commandToRun = _showCommandProxy.GetImportModuleCommand(_showCommandProxy.ParentModuleNeedingImportModule);
|
|
Collection<PSObject> rawCommands;
|
|
try
|
|
{
|
|
rawCommands = this.InvokeCommand.InvokeScript(commandToRun);
|
|
}
|
|
catch (RuntimeException e)
|
|
{
|
|
_showCommandProxy.ImportModuleFailed(e);
|
|
continue;
|
|
}
|
|
|
|
_commands = _showCommandProxy.GetCommandList((object[])rawCommands[0].BaseObject);
|
|
_importedModules = _showCommandProxy.GetImportedModulesDictionary((object[])rawCommands[1].BaseObject);
|
|
_showCommandProxy.ImportModuleDone(_importedModules, _commands);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes the output of a script being run into the pipeline.
|
|
/// </summary>
|
|
/// <param name="sender">Output collection.</param>
|
|
/// <param name="e">Output event.</param>
|
|
private void Output_DataAdded(object sender, DataAddedEventArgs e)
|
|
{
|
|
this.WriteObject(((PSDataCollection<object>)sender)[e.Index]);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes the errors of a script being run into the pipeline.
|
|
/// </summary>
|
|
/// <param name="sender">Error collection.</param>
|
|
/// <param name="e">Error event.</param>
|
|
private void Error_DataAdded(object sender, DataAddedEventArgs e)
|
|
{
|
|
this.WriteError(((PSDataCollection<ErrorRecord>)sender)[e.Index]);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Implements IDisposable logic.
|
|
/// </summary>
|
|
/// <param name="isDisposing">True if being called from Dispose.</param>
|
|
private void Dispose(bool isDisposing)
|
|
{
|
|
if (isDisposing)
|
|
{
|
|
if (_errors != null)
|
|
{
|
|
_errors.Dispose();
|
|
_errors = null;
|
|
}
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
/// <summary>
|
|
/// Wraps interop code for console input buffer.
|
|
/// </summary>
|
|
internal static class ConsoleInputWithNativeMethods
|
|
{
|
|
/// <summary>
|
|
/// Constant used in calls to GetStdHandle.
|
|
/// </summary>
|
|
internal const int STD_INPUT_HANDLE = -10;
|
|
|
|
/// <summary>
|
|
/// Adds a string to the console input buffer.
|
|
/// </summary>
|
|
/// <param name="str">String to add to console input buffer.</param>
|
|
/// <param name="newLine">True to add Enter after the string.</param>
|
|
/// <returns>True if it was successful in adding all characters to console input buffer.</returns>
|
|
internal static bool AddToConsoleInputBuffer(string str, bool newLine)
|
|
{
|
|
IntPtr handle = ConsoleInputWithNativeMethods.GetStdHandle(ConsoleInputWithNativeMethods.STD_INPUT_HANDLE);
|
|
if (handle == IntPtr.Zero)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
uint strLen = (uint)str.Length;
|
|
|
|
ConsoleInputWithNativeMethods.INPUT_RECORD[] records = new ConsoleInputWithNativeMethods.INPUT_RECORD[strLen + (newLine ? 1 : 0)];
|
|
|
|
for (int i = 0; i < strLen; i++)
|
|
{
|
|
ConsoleInputWithNativeMethods.INPUT_RECORD.SetInputRecord(ref records[i], str[i]);
|
|
}
|
|
|
|
uint written;
|
|
if (!ConsoleInputWithNativeMethods.WriteConsoleInput(handle, records, strLen, out written) || written != strLen)
|
|
{
|
|
// I do not know of a case where written is not going to be strlen. Maybe for some character that
|
|
// is not supported in the console. The API suggests this can happen,
|
|
// so we handle it by returning false
|
|
return false;
|
|
}
|
|
|
|
// Enter is written separately, because if this is a command, and one of the characters in the command was not written
|
|
// (written != strLen) it is desireable to fail (return false) before typing enter and running the command
|
|
if (newLine)
|
|
{
|
|
ConsoleInputWithNativeMethods.INPUT_RECORD[] enterArray = new ConsoleInputWithNativeMethods.INPUT_RECORD[1];
|
|
ConsoleInputWithNativeMethods.INPUT_RECORD.SetInputRecord(ref enterArray[0], (char)13);
|
|
|
|
written = 0;
|
|
if (!ConsoleInputWithNativeMethods.WriteConsoleInput(handle, enterArray, 1, out written))
|
|
{
|
|
// I don't think this will happen
|
|
return false;
|
|
}
|
|
|
|
Diagnostics.Assert(written == 1, "only Enter is being added and it is a supported character");
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the console handle.
|
|
/// </summary>
|
|
/// <param name="nStdHandle">Which console handle to get.</param>
|
|
/// <returns>The console handle.</returns>
|
|
[DllImport("kernel32.dll", SetLastError = true)]
|
|
internal static extern IntPtr GetStdHandle(int nStdHandle);
|
|
|
|
/// <summary>
|
|
/// Writes to the console input buffer.
|
|
/// </summary>
|
|
/// <param name="hConsoleInput">Console handle.</param>
|
|
/// <param name="lpBuffer">Inputs to be written.</param>
|
|
/// <param name="nLength">Number of inputs to be written.</param>
|
|
/// <param name="lpNumberOfEventsWritten">Returned number of inputs actually written.</param>
|
|
/// <returns>0 if the function fails.</returns>
|
|
[DllImport("kernel32.dll", SetLastError = true)]
|
|
[return: MarshalAs(UnmanagedType.Bool)]
|
|
internal static extern bool WriteConsoleInput(
|
|
IntPtr hConsoleInput,
|
|
INPUT_RECORD[] lpBuffer,
|
|
uint nLength,
|
|
out uint lpNumberOfEventsWritten);
|
|
|
|
/// <summary>
|
|
/// A record to be added to the console buffer.
|
|
/// </summary>
|
|
internal struct INPUT_RECORD
|
|
{
|
|
/// <summary>
|
|
/// The proper event type for a KeyEvent KEY_EVENT_RECORD.
|
|
/// </summary>
|
|
internal const int KEY_EVENT = 0x0001;
|
|
|
|
/// <summary>
|
|
/// Input buffer event type.
|
|
/// </summary>
|
|
internal ushort EventType;
|
|
|
|
/// <summary>
|
|
/// The actual event. The original structure is a union of many others, but this is the largest of them.
|
|
/// And we don't need other kinds of events.
|
|
/// </summary>
|
|
internal KEY_EVENT_RECORD KeyEvent;
|
|
|
|
/// <summary>
|
|
/// Sets the necessary fields of <paramref name="inputRecord"/> for a KeyDown event for the <paramref name="character"/>
|
|
/// </summary>
|
|
/// <param name="inputRecord">Input record to be set.</param>
|
|
/// <param name="character">Character to set the record with.</param>
|
|
internal static void SetInputRecord(ref INPUT_RECORD inputRecord, char character)
|
|
{
|
|
inputRecord.EventType = INPUT_RECORD.KEY_EVENT;
|
|
inputRecord.KeyEvent.bKeyDown = true;
|
|
inputRecord.KeyEvent.UnicodeChar = character;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Type of INPUT_RECORD which is a key.
|
|
/// </summary>
|
|
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
|
internal struct KEY_EVENT_RECORD
|
|
{
|
|
/// <summary>
|
|
/// True for key down and false for key up, but only needed if wVirtualKeyCode is used.
|
|
/// </summary>
|
|
internal bool bKeyDown;
|
|
|
|
/// <summary>
|
|
/// Repeat count.
|
|
/// </summary>
|
|
internal ushort wRepeatCount;
|
|
|
|
/// <summary>
|
|
/// Virtual key code.
|
|
/// </summary>
|
|
internal ushort wVirtualKeyCode;
|
|
|
|
/// <summary>
|
|
/// Virtual key scan code.
|
|
/// </summary>
|
|
internal ushort wVirtualScanCode;
|
|
|
|
/// <summary>
|
|
/// Character in input. If this is specified, wVirtualKeyCode, and others don't need to be.
|
|
/// </summary>
|
|
internal char UnicodeChar;
|
|
|
|
/// <summary>
|
|
/// State of keys like Shift and control.
|
|
/// </summary>
|
|
internal uint dwControlKeyState;
|
|
}
|
|
}
|
|
}
|
|
}
|