// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
// Code forked from Betsegaw Tadele's https://github.com/betsegaw/windowwalker/
using System.Diagnostics;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.CmdPal.Ext.WindowWalker.Components;
///
/// Represents a specific open window
///
internal sealed class Window
{
///
/// A static cache for the process data of all known windows
/// that we don't have to query the data every time
///
private static readonly Dictionary HandlesToProcessCache = new();
///
/// Gets the title of the window (the string displayed at the top of the window)
///
internal string Title
{
get
{
var sizeOfTitle = NativeMethods.GetWindowTextLength(Hwnd);
if (sizeOfTitle++ > 0)
{
var titleBuffer = new StringBuilder(sizeOfTitle);
var numCharactersWritten = NativeMethods.GetWindowText(Hwnd, titleBuffer, sizeOfTitle);
if (numCharactersWritten == 0)
{
return string.Empty;
}
return titleBuffer.ToString();
}
else
{
return string.Empty;
}
}
}
///
/// Gets the handle to the window
///
internal IntPtr Hwnd { get; }
///
/// Gets the object of with the process information of the window
///
internal WindowProcess Process { get; }
///
/// Gets the object of with the desktop information of the window
///
internal VDesktop Desktop { get; }
///
/// Gets the name of the class for the window represented
///
internal string ClassName => GetWindowClassName(Hwnd);
///
/// Gets a value indicating whether the window is visible (might return false if it is a hidden IE tab)
///
internal bool Visible => NativeMethods.IsWindowVisible(Hwnd);
///
/// Gets a value indicating whether the window is cloaked (true) or not (false).
/// (A cloaked window is not visible to the user. But the window is still composed by DWM.)
///
internal bool IsCloaked => GetWindowCloakState() != WindowCloakState.None;
///
/// Gets a value indicating whether the specified window handle identifies an existing window.
///
internal bool IsWindow => NativeMethods.IsWindow(Hwnd);
///
/// Gets a value indicating whether the window is a toolwindow
///
internal bool IsToolWindow => (NativeMethods.GetWindowLong(Hwnd, Win32Constants.GWL_EXSTYLE) &
(uint)ExtendedWindowStyles.WS_EX_TOOLWINDOW) ==
(uint)ExtendedWindowStyles.WS_EX_TOOLWINDOW;
///
/// Gets a value indicating whether the window is an appwindow
///
internal bool IsAppWindow => (NativeMethods.GetWindowLong(Hwnd, Win32Constants.GWL_EXSTYLE) &
(uint)ExtendedWindowStyles.WS_EX_APPWINDOW) ==
(uint)ExtendedWindowStyles.WS_EX_APPWINDOW;
///
/// Gets a value indicating whether the window has ITaskList_Deleted property
///
internal bool TaskListDeleted => NativeMethods.GetProp(Hwnd, "ITaskList_Deleted") != IntPtr.Zero;
///
/// Gets a value indicating whether the specified windows is the owner (i.e. doesn't have an owner)
///
internal bool IsOwner => NativeMethods.GetWindow(Hwnd, GetWindowCmd.GW_OWNER) == IntPtr.Zero;
///
/// Gets a value indicating whether the window is minimized
///
private bool Minimized => GetWindowSizeState() == WindowSizeState.Minimized;
///
/// Initializes a new instance of the class.
/// Initializes a new Window representation
///
/// the handle to the window we are representing
internal Window(IntPtr hwnd)
{
// TODO: Add verification as to whether the window handle is valid
Hwnd = hwnd;
Process = CreateWindowProcessInstance(hwnd);
Desktop = WindowWalkerCommandsProvider.VirtualDesktopHelperInstance.GetWindowDesktop(hwnd);
}
///
/// Switches desktop focus to the window
///
internal void SwitchToWindow()
{
// The following block is necessary because
// 1) There is a weird flashing behavior when trying
// to use ShowWindow for switching tabs in IE
// 2) SetForegroundWindow fails on minimized windows
// Using Ordinal since this is internal
if (Process.Name?.ToUpperInvariant().Equals("IEXPLORE.EXE", StringComparison.Ordinal) == true || !Minimized)
{
NativeMethods.SetForegroundWindow(Hwnd);
}
else
{
if (!NativeMethods.ShowWindow(Hwnd, ShowWindowCommand.Restore))
{
// ShowWindow doesn't work if the process is running elevated: fall back to SendMessage
_ = NativeMethods.SendMessage(Hwnd, Win32Constants.WM_SYSCOMMAND, Win32Constants.SC_RESTORE);
}
}
NativeMethods.FlashWindow(Hwnd, true);
}
///
/// Helper function to close the window
///
private void CloseThisWindowHelper()
{
_ = NativeMethods.SendMessageTimeout(Hwnd, Win32Constants.WM_SYSCOMMAND, Win32Constants.SC_CLOSE, 0, 0x0000, 5000, out _);
}
///
/// Closes the window
///
internal void CloseThisWindow()
{
Thread thread = new(CloseThisWindowHelper);
thread.Start();
}
///
/// Tries to get the window icon.
///
/// The window icon if found; otherwise, null.
/// True if an icon was found; otherwise, false.
internal bool TryGetWindowIcon(out System.Drawing.Icon? icon)
{
icon = null;
if (Hwnd == IntPtr.Zero)
{
return false;
}
// Try WM_GETICON with SendMessageTimeout
if (NativeMethods.SendMessageTimeout(Hwnd, Win32Constants.WM_GETICON, Win32Constants.ICON_BIG, IntPtr.Zero, Win32Constants.SMTO_ABORTIFHUNG, 100, out var result) != 0 && result != 0)
{
icon = System.Drawing.Icon.FromHandle(result);
NativeMethods.DestroyIcon(result);
return true;
}
if (NativeMethods.SendMessageTimeout(Hwnd, Win32Constants.WM_GETICON, Win32Constants.ICON_SMALL, IntPtr.Zero, Win32Constants.SMTO_ABORTIFHUNG, 100, out result) != 0 && result != 0)
{
icon = System.Drawing.Icon.FromHandle(result);
NativeMethods.DestroyIcon(result);
return true;
}
if (NativeMethods.SendMessageTimeout(Hwnd, Win32Constants.WM_GETICON, Win32Constants.ICON_SMALL2, IntPtr.Zero, Win32Constants.SMTO_ABORTIFHUNG, 100, out result) != 0 && result != 0)
{
icon = System.Drawing.Icon.FromHandle(result);
NativeMethods.DestroyIcon(result);
return true;
}
// Fallback to GetClassLongPtr
var iconHandle = NativeMethods.GetClassLongPtr(Hwnd, Win32Constants.GCLP_HICON);
if (iconHandle != IntPtr.Zero)
{
icon = System.Drawing.Icon.FromHandle(iconHandle);
NativeMethods.DestroyIcon(iconHandle);
return true;
}
iconHandle = NativeMethods.GetClassLongPtr(Hwnd, Win32Constants.GCLP_HICONSM);
if (iconHandle != IntPtr.Zero)
{
icon = System.Drawing.Icon.FromHandle(iconHandle);
NativeMethods.DestroyIcon(iconHandle);
return true;
}
return false;
}
///
/// Converts the window name to string along with the process name
///
/// The title of the window
public override string ToString()
{
// Using CurrentCulture since this is user facing
return Title + " (" + Process.Name?.ToUpper(CultureInfo.CurrentCulture) + ")";
}
///
/// Returns what the window size is
///
/// The state (minimized, maximized, etc...) of the window
private WindowSizeState GetWindowSizeState()
{
NativeMethods.GetWindowPlacement(Hwnd, out var placement);
switch (placement.ShowCmd)
{
case ShowWindowCommand.Normal:
return WindowSizeState.Normal;
case ShowWindowCommand.Minimize:
case ShowWindowCommand.ShowMinimized:
return WindowSizeState.Minimized;
case ShowWindowCommand.Maximize: // No need for ShowMaximized here since its also of value 3
return WindowSizeState.Maximized;
default:
// throw new Exception("Don't know how to handle window state = " + placement.ShowCmd);
return WindowSizeState.Unknown;
}
}
///
/// Enum to simplify the state of the window
///
internal enum WindowSizeState
{
Normal,
Minimized,
Maximized,
Unknown,
}
///
/// Returns the window cloak state from DWM
/// (A cloaked window is not visible to the user. But the window is still composed by DWM.)
///
/// The state (none, app, ...) of the window
internal WindowCloakState GetWindowCloakState()
{
_ = NativeMethods.DwmGetWindowAttribute(Hwnd, (int)DwmWindowAttributes.Cloaked, out var isCloakedState, sizeof(uint));
switch (isCloakedState)
{
case (int)DwmWindowCloakStates.None:
return WindowCloakState.None;
case (int)DwmWindowCloakStates.CloakedApp:
return WindowCloakState.App;
case (int)DwmWindowCloakStates.CloakedShell:
return WindowWalkerCommandsProvider.VirtualDesktopHelperInstance.IsWindowCloakedByVirtualDesktopManager(Hwnd, Desktop.Id) ? WindowCloakState.OtherDesktop : WindowCloakState.Shell;
case (int)DwmWindowCloakStates.CloakedInherited:
return WindowCloakState.Inherited;
default:
return WindowCloakState.Unknown;
}
}
///
/// Enum to simplify the cloak state of the window
///
internal enum WindowCloakState
{
None,
App,
Shell,
Inherited,
OtherDesktop,
Unknown,
}
///
/// Returns the class name of a window.
///
/// Handle to the window.
/// Class name
private static string GetWindowClassName(IntPtr hwnd)
{
var windowClassName = new StringBuilder(300);
var numCharactersWritten = NativeMethods.GetClassName(hwnd, windowClassName, windowClassName.MaxCapacity);
if (numCharactersWritten == 0)
{
return string.Empty;
}
return windowClassName.ToString();
}
///
/// Gets an instance of form process cache or creates a new one. A new one will be added to the cache.
///
/// The handle to the window
/// A new Instance of type
private static WindowProcess CreateWindowProcessInstance(IntPtr hWindow)
{
lock (HandlesToProcessCache)
{
if (HandlesToProcessCache.Count > 7000)
{
Debug.Print("Clearing Process Cache because it's size is " + HandlesToProcessCache.Count);
HandlesToProcessCache.Clear();
}
// Add window's process to cache if missing
if (!HandlesToProcessCache.ContainsKey(hWindow))
{
// Get process ID and name
var processId = WindowProcess.GetProcessIDFromWindowHandle(hWindow);
var threadId = WindowProcess.GetThreadIDFromWindowHandle(hWindow);
var processName = WindowProcess.GetProcessNameFromProcessID(processId);
if (processName.Length != 0)
{
HandlesToProcessCache.Add(hWindow, new WindowProcess(processId, threadId, processName));
}
else
{
// For the dwm process we cannot receive the name. This is no problem because the window isn't part of result list.
ExtensionHost.LogMessage(new LogMessage { Message = $"Invalid process {processId} ({processName}) for window handle {hWindow}." });
HandlesToProcessCache.Add(hWindow, new WindowProcess(0, 0, string.Empty));
}
}
// Correct the process data if the window belongs to a uwp app hosted by 'ApplicationFrameHost.exe'
// (This only works if the window isn't minimized. For minimized windows the required child window isn't assigned.)
if (HandlesToProcessCache[hWindow].IsUwpAppFrameHost)
{
new Task(() =>
{
var callbackptr = new EnumWindowsProc((hwnd, _) =>
{
// Every uwp app main window has at least three child windows. Only the one we are interested in has a class starting with "Windows.UI.Core." and is assigned to the real app process.
// (The other ones have a class name that begins with the string "ApplicationFrame".)
if (GetWindowClassName(hwnd).StartsWith("Windows.UI.Core.", StringComparison.OrdinalIgnoreCase))
{
var childProcessId = WindowProcess.GetProcessIDFromWindowHandle(hwnd);
var childThreadId = WindowProcess.GetThreadIDFromWindowHandle(hwnd);
var childProcessName = WindowProcess.GetProcessNameFromProcessID(childProcessId);
// Update process info in cache
lock (HandlesToProcessCache)
{
HandlesToProcessCache[hWindow].UpdateProcessInfo(childProcessId, childThreadId, childProcessName);
}
return false;
}
else
{
return true;
}
});
_ = NativeMethods.EnumChildWindows(hWindow, callbackptr, 0);
}).Start();
}
return HandlesToProcessCache[hWindow];
}
}
}