Electron support for Linux (#1074)

* init

* SQLite changes

* Move html folder, edit build scripts

* AppApi interface

* Build flags

* AppApi inheritance

* Finishing touches

* Merge upstream changes

* Test CI

* Fix class inits

* Rename AppApi

* Merge upstream changes

* Fix SQLiteLegacy on Linux, Add Linux interop, build tools

* Linux specific localisation strings

* Make it run

* Bring back most of Linux functionality

* Clean up

* Fix TTS voices

* Fix UI var

* Changes

* Electron minimise to tray

* Remove separate toggle for WlxOverlay

* Fixes

* Touchups

* Move csproj

* Window zoom, Desktop Notifications, VR check on Linux

* Fix desktop notifications, VR check spam

* Fix building on Linux

* Clean up

* Fix WebApi headers

* Rewrite VRCX updater

* Clean up

* Linux updater

* Add Linux to build action

* init

* SQLite changes

* Move html folder, edit build scripts

* AppApi interface

* Build flags

* AppApi inheritance

* Finishing touches

* Merge upstream changes

* Test CI

* Fix class inits

* Rename AppApi

* Merge upstream changes

* Fix SQLiteLegacy on Linux, Add Linux interop, build tools

* Linux specific localisation strings

* Make it run

* Bring back most of Linux functionality

* Clean up

* Fix TTS voices

* Changes

* Electron minimise to tray

* Remove separate toggle for WlxOverlay

* Fixes

* Touchups

* Move csproj

* Window zoom, Desktop Notifications, VR check on Linux

* Fix desktop notifications, VR check spam

* Fix building on Linux

* Clean up

* Fix WebApi headers

* Rewrite VRCX updater

* Clean up

* Linux updater

* Add Linux to build action

* Test updater

* Rebase and handle merge conflicts

* Fix Linux updater

* Fix Linux app restart

* Fix friend order

* Handle AppImageInstaller, show an install message on Linux

* Updates to the AppImage installer

* Fix Linux updater, fix set version, check for .NET, copy wine prefix

* Handle random errors

* Rotate tall prints

* try fix Linux restart bug

* Final

---------

Co-authored-by: rs189 <35667100+rs189@users.noreply.github.com>
This commit is contained in:
Natsumi
2025-01-11 13:09:44 +13:00
committed by GitHub
parent a39eb9d5ed
commit 938fff63d0
223 changed files with 15841 additions and 9562 deletions

View File

@@ -26,7 +26,7 @@ namespace VRCX
}
// forgive me father for i have sinned once again
AppApi.Instance.ExecuteAppFunction("dragEnterCef", file);
Program.AppApiInstance.ExecuteAppFunction("dragEnterCef", file);
dragData.Dispose();
return false;
}

View File

@@ -1,16 +1,16 @@
using System;
using System.IO;
using System.Net;
using CefSharp;
using CefSharp.SchemeHandler;
using CefSharp.WinForms;
using NLog;
namespace VRCX
{
public class CefService
{
public static readonly CefService Instance;
private static readonly NLog.Logger logger = NLog.LogManager.GetLogger("VRCX");
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
static CefService()
{

View File

@@ -0,0 +1,26 @@
using CefSharp;
namespace VRCX
{
public static class JavascriptBindings
{
public static void ApplyAppJavascriptBindings(IJavascriptObjectRepository repository)
{
repository.NameConverter = null;
repository.Register("AppApi", Program.AppApiInstance);
repository.Register("SharedVariable", SharedVariable.Instance);
repository.Register("WebApi", WebApi.Instance);
repository.Register("VRCXStorage", VRCXStorage.Instance);
repository.Register("SQLite", SQLiteLegacy.Instance);
repository.Register("LogWatcher", LogWatcher.Instance);
repository.Register("Discord", Discord.Instance);
repository.Register("AssetBundleManager", AssetBundleManager.Instance);
}
public static void ApplyVrJavascriptBindings(IJavascriptObjectRepository repository)
{
repository.NameConverter = null;
repository.Register("AppApiVr", AppApiVr.Instance);
}
}
}

117
Dotnet/Cef/MainForm.Designer.cs generated Normal file
View File

@@ -0,0 +1,117 @@
// Copyright(c) 2019 pypy. All rights reserved.
//
// This work is licensed under the terms of the MIT license.
// For a copy, see <https://opensource.org/licenses/MIT>.
namespace VRCX
{
partial class MainForm
{
/// <summary>
/// 필수 디자이너 변수입니다.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// 사용 중인 모든 리소스를 정리합니다.
/// </summary>
/// <param name="disposing">관리되는 리소스를 삭제해야 하면 true이고, 그렇지 않으면 false입니다.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form
/// <summary>
/// 디자이너 지원에 필요한 메서드입니다.
/// 이 메서드의 내용을 코드 편집기로 수정하지 마세요.
/// </summary>
private void InitializeComponent()
{
this.components = new System.ComponentModel.Container();
this.TrayMenu = new System.Windows.Forms.ContextMenuStrip(this.components);
this.TrayMenu_Open = new System.Windows.Forms.ToolStripMenuItem();
this.TrayMenu_DevTools = new System.Windows.Forms.ToolStripMenuItem();
this.TrayMenu_Separator = new System.Windows.Forms.ToolStripSeparator();
this.TrayMenu_Quit = new System.Windows.Forms.ToolStripMenuItem();
this.TrayIcon = new System.Windows.Forms.NotifyIcon(this.components);
this.TrayMenu.SuspendLayout();
this.SuspendLayout();
//
// TrayMenu
//
this.TrayMenu.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.TrayMenu_Open,
this.TrayMenu_DevTools,
this.TrayMenu_Separator,
this.TrayMenu_Quit});
this.TrayMenu.Name = "TrayMenu";
this.TrayMenu.Size = new System.Drawing.Size(132, 54);
//
// TrayMenu_Open
//
this.TrayMenu_Open.Name = "TrayMenu_Open";
this.TrayMenu_Open.Size = new System.Drawing.Size(131, 22);
this.TrayMenu_Open.Text = "Open";
this.TrayMenu_Open.Click += new System.EventHandler(this.TrayMenu_Open_Click);
//
// TrayMenu_DevTools
//
this.TrayMenu_DevTools.Name = "TrayMenu_DevTools";
this.TrayMenu_DevTools.Size = new System.Drawing.Size(131, 22);
this.TrayMenu_DevTools.Text = "DevTools";
this.TrayMenu_DevTools.Click += new System.EventHandler(this.TrayMenu_DevTools_Click);
//
// TrayMenu_Separator
//
this.TrayMenu_Separator.Name = "TrayMenu_Separator";
this.TrayMenu_Separator.Size = new System.Drawing.Size(128, 6);
//
// TrayMenu_Quit
//
this.TrayMenu_Quit.Name = "TrayMenu_Quit";
this.TrayMenu_Quit.Size = new System.Drawing.Size(131, 22);
this.TrayMenu_Quit.Text = "Quit VRCX";
this.TrayMenu_Quit.Click += new System.EventHandler(this.TrayMenu_Quit_Click);
//
// TrayIcon
//
this.TrayIcon.ContextMenuStrip = this.TrayMenu;
this.TrayIcon.Text = "VRCX";
this.TrayIcon.Visible = true;
this.TrayIcon.MouseClick += new System.Windows.Forms.MouseEventHandler(this.TrayIcon_MouseClick);
//
// MainForm
//
this.AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;
this.ClientSize = new System.Drawing.Size(842, 561);
this.MinimumSize = new System.Drawing.Size(320, 240);
this.Name = "MainForm";
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
this.Text = Program.Version;
this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.MainForm_FormClosing);
this.FormClosed += new System.Windows.Forms.FormClosedEventHandler(this.MainForm_FormClosed);
this.Load += new System.EventHandler(this.MainForm_Load);
this.Move += new System.EventHandler(this.MainForm_Move);
this.Resize += new System.EventHandler(this.MainForm_Resize);
this.TrayMenu.ResumeLayout(false);
this.ResumeLayout(false);
}
#endregion
private System.Windows.Forms.ContextMenuStrip TrayMenu;
private System.Windows.Forms.ToolStripMenuItem TrayMenu_Open;
private System.Windows.Forms.ToolStripMenuItem TrayMenu_DevTools;
private System.Windows.Forms.ToolStripSeparator TrayMenu_Separator;
private System.Windows.Forms.ToolStripMenuItem TrayMenu_Quit;
private System.Windows.Forms.NotifyIcon TrayIcon;
}
}

247
Dotnet/Cef/MainForm.cs Normal file
View File

@@ -0,0 +1,247 @@
// Copyright(c) 2019-2022 pypy, Natsumi and individual contributors.
// All rights reserved.
//
// This work is licensed under the terms of the MIT license.
// For a copy, see <https://opensource.org/licenses/MIT>.
using System;
using System.Drawing;
using System.Net;
using System.Reflection;
using System.Windows.Forms;
using CefSharp;
using CefSharp.WinForms;
using NLog;
namespace VRCX
{
public partial class MainForm : WinformBase
{
public static MainForm Instance;
public static NativeWindow nativeWindow;
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
public ChromiumWebBrowser Browser;
private readonly Timer _saveTimer;
private int LastLocationX;
private int LastLocationY;
private int LastSizeWidth;
private int LastSizeHeight;
private FormWindowState _LastWindowStateToRestore = FormWindowState.Normal;
private FormWindowState LastWindowStateToRestore
{
get => _LastWindowStateToRestore;
set
{
// Used to restore window state after minimized
if (FormWindowState.Minimized != value)
{
_LastWindowStateToRestore = value;
}
}
}
public MainForm()
{
Instance = this;
InitializeComponent();
nativeWindow = NativeWindow.FromHandle(this.Handle);
// adding a 5s delay here to avoid excessive writes to disk
_saveTimer = new Timer();
_saveTimer.Interval = 5000;
_saveTimer.Tick += SaveTimer_Tick;
try
{
var location = Assembly.GetExecutingAssembly().Location;
var icon = Icon.ExtractAssociatedIcon(location);
Icon = icon;
TrayIcon.Icon = icon;
}
catch (Exception ex)
{
logger.Error(ex);
}
Browser = new ChromiumWebBrowser("file://vrcx/index.html")
{
DragHandler = new CustomDragHandler(),
MenuHandler = new CustomMenuHandler(),
DownloadHandler = new CustomDownloadHandler(),
BrowserSettings =
{
DefaultEncoding = "UTF-8",
},
Dock = DockStyle.Fill
};
Browser.IsBrowserInitializedChanged += (A, B) =>
{
if (Program.LaunchDebug)
Browser.ShowDevTools();
};
JavascriptBindings.ApplyAppJavascriptBindings(Browser.JavascriptObjectRepository);
Browser.ConsoleMessage += (_, args) =>
{
logger.Debug(args.Message + " (" + args.Source + ":" + args.Line + ")");
};
Controls.Add(Browser);
}
private void MainForm_Load(object sender, System.EventArgs e)
{
try
{
int.TryParse(VRCXStorage.Instance.Get("VRCX_LocationX"), out LastLocationX);
int.TryParse(VRCXStorage.Instance.Get("VRCX_LocationY"), out LastLocationY);
int.TryParse(VRCXStorage.Instance.Get("VRCX_SizeWidth"), out LastSizeWidth);
int.TryParse(VRCXStorage.Instance.Get("VRCX_SizeHeight"), out LastSizeHeight);
var location = new Point(LastLocationX, LastLocationY);
var size = new Size(LastSizeWidth, LastSizeHeight);
var screen = Screen.FromPoint(location);
if (screen.Bounds.Contains(location.X, location.Y))
{
Location = location;
}
Size = new Size(1920, 1080);
if (size.Width > 0 && size.Height > 0)
{
Size = size;
}
}
catch (Exception ex)
{
logger.Error(ex);
}
try
{
var state = WindowState;
if (int.TryParse(VRCXStorage.Instance.Get("VRCX_WindowState"), out int v))
{
state = (FormWindowState)v;
}
if (state == FormWindowState.Minimized)
{
state = FormWindowState.Normal;
}
if ("true".Equals(VRCXStorage.Instance.Get("VRCX_StartAsMinimizedState")))
{
state = FormWindowState.Minimized;
}
if ("true".Equals(VRCXStorage.Instance.Get("VRCX_StartAsMinimizedState")) &&
"true".Equals(VRCXStorage.Instance.Get("VRCX_CloseToTray")))
{
BeginInvoke(Hide);
}
else
{
WindowState = state;
}
}
catch (Exception ex)
{
logger.Error(ex);
}
LastWindowStateToRestore = WindowState;
// 가끔 화면 위치가 안맞음.. 이걸로 해결 될지는 모르겠음
Browser.Invalidate();
}
private void MainForm_Resize(object sender, System.EventArgs e)
{
LastWindowStateToRestore = WindowState;
if (WindowState != FormWindowState.Normal)
{
return;
}
LastSizeWidth = Size.Width;
LastSizeHeight = Size.Height;
_saveTimer?.Start();
}
private void SaveTimer_Tick(object sender, EventArgs e)
{
SaveWindowState();
_saveTimer?.Stop();
}
private void MainForm_Move(object sender, System.EventArgs e)
{
if (WindowState != FormWindowState.Normal)
{
return;
}
LastLocationX = Location.X;
LastLocationY = Location.Y;
_saveTimer?.Start();
}
private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
{
if (e.CloseReason == CloseReason.UserClosing &&
"true".Equals(VRCXStorage.Instance.Get("VRCX_CloseToTray")))
{
e.Cancel = true;
Hide();
}
}
private void SaveWindowState()
{
VRCXStorage.Instance.Set("VRCX_LocationX", LastLocationX.ToString());
VRCXStorage.Instance.Set("VRCX_LocationY", LastLocationY.ToString());
VRCXStorage.Instance.Set("VRCX_SizeWidth", LastSizeWidth.ToString());
VRCXStorage.Instance.Set("VRCX_SizeHeight", LastSizeHeight.ToString());
VRCXStorage.Instance.Set("VRCX_WindowState", ((int)WindowState).ToString());
VRCXStorage.Instance.Flush();
}
private void MainForm_FormClosed(object sender, FormClosedEventArgs e)
{
SaveWindowState();
}
private void TrayIcon_MouseClick(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
Focus_Window();
}
}
private void TrayMenu_Open_Click(object sender, System.EventArgs e)
{
Focus_Window();
}
public void Focus_Window()
{
Show();
if (WindowState == FormWindowState.Minimized)
{
WindowState = LastWindowStateToRestore;
}
// Focus();
Activate();
}
private void TrayMenu_DevTools_Click(object sender, System.EventArgs e)
{
Instance.Browser.ShowDevTools();
}
private void TrayMenu_Quit_Click(object sender, System.EventArgs e)
{
SaveWindowState();
Application.Exit();
}
}
}

126
Dotnet/Cef/MainForm.resx Normal file
View File

@@ -0,0 +1,126 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<metadata name="TrayMenu.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>17, 17</value>
</metadata>
<metadata name="TrayIcon.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>124, 17</value>
</metadata>
</root>

23
Dotnet/Cef/Wine.cs Normal file
View File

@@ -0,0 +1,23 @@
using System;
using System.Runtime.InteropServices;
namespace VRCX
{
public static class Wine
{
[DllImport("ntdll.dll")]
private static extern IntPtr wine_get_version();
public static bool GetIfWine()
{
// wine_get_version should be guaranteed to exist exclusively in Wine envs,
// unlike some other suggestions like checking Wine registry keys
try
{
wine_get_version();
return true;
}
catch { return false; }
}
}
}

19
Dotnet/Cef/WinformBase.cs Normal file
View File

@@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace VRCX
{
public class WinformBase : Form
{
protected override void OnHandleCreated(EventArgs e)
{
WinformThemer.SetThemeToGlobal(this);
base.OnHandleCreated(e);
}
}
}

192
Dotnet/Cef/WinformThemer.cs Normal file
View File

@@ -0,0 +1,192 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Windows.Forms;
namespace VRCX
{
//Based off DWMWA_USE_IMMERSIVE_DARK_MODE, documentation: https://docs.microsoft.com/en-us/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute
//dwAttribute was 19 before Windows 20H1, 20 after Windows 20H1
internal static class WinformThemer
{
/// <summary>
/// Flash both the window caption and taskbar button.
/// This is equivalent to setting the FLASHW_CAPTION | FLASHW_TRAY flags.
/// </summary>
public const uint FLASHW_ALL = 3;
/// <summary>
/// Flash continuously until the window comes to the foreground.
/// </summary>
public const uint FLASHW_TIMERNOFG = 12;
/// <summary>
/// Private holder of current theme
/// </summary>
private static int currentTheme;
/// <summary>
/// Sets the global theme of the app
/// Light = 0
/// Dark = 1
/// </summary>
public static void SetGlobalTheme(int theme)
{
currentTheme = theme;
//Make a seperate list for all current forms (causes issues otherwise)
var forms = new List<Form>();
foreach (Form form in Application.OpenForms)
{
forms.Add(form);
}
SetThemeToGlobal(forms);
}
/// <summary>
/// Gets the global theme of the app
/// Light = 0
/// Dark = 1
/// </summary>
public static int GetGlobalTheme()
{
return currentTheme;
}
/// <summary>
/// Set given form to the current global theme
/// </summary>
/// <param name="form"></param>
public static void SetThemeToGlobal(Form form)
{
SetThemeToGlobal(new List<Form> { form });
}
/// <summary>
/// Set a list of given forms to the current global theme
/// </summary>
/// <param name="forms"></param>
public static void SetThemeToGlobal(List<Form> forms)
{
MainForm.Instance.Invoke(new Action(() =>
{
//For each form, set the theme, then move focus onto it to force refresh
foreach (var form in forms)
{
//Set the theme of the window
SetThemeToGlobal(form.Handle);
//Change opacity to foce full redraw
form.Opacity = 0.99999;
form.Opacity = 1;
}
}));
}
private static void SetThemeToGlobal(IntPtr handle)
{
if (GetTheme(handle) != currentTheme)
{
if (PInvoke.DwmSetWindowAttribute(handle, 19, new[] { currentTheme }, 4) != 0)
PInvoke.DwmSetWindowAttribute(handle, 20, new[] { currentTheme }, 4);
}
}
private static int GetTheme(IntPtr handle)
{
//Allocate needed memory
var curThemePtr = Marshal.AllocHGlobal(4);
//See what window state it currently is
if (PInvoke.DwmGetWindowAttribute(handle, 19, curThemePtr, 4) != 0)
PInvoke.DwmGetWindowAttribute(handle, 20, curThemePtr, 4);
//Read current theme (light = 0, dark = 1)
var theme = Marshal.ReadInt32(curThemePtr);
//Free previously allocated
Marshal.FreeHGlobal(curThemePtr);
return theme;
}
public static void DoFunny()
{
foreach (Form form in Application.OpenForms)
{
PInvoke.SetWindowLong(form.Handle, -20, 0x00C00000);
// PInvoke.SetWindowLong(form.Handle, -20, 0x00050100);
}
}
private static FLASHWINFO Create_FLASHWINFO(IntPtr handle, uint flags, uint count, uint timeout)
{
var fi = new FLASHWINFO();
fi.cbSize = Convert.ToUInt32(Marshal.SizeOf(fi));
fi.hwnd = handle;
fi.dwFlags = flags;
fi.uCount = count;
fi.dwTimeout = timeout;
return fi;
}
/// <summary>
/// Flash the spacified Window (Form) until it receives focus.
/// </summary>
/// <param name="form">The Form (Window) to Flash.</param>
/// <returns></returns>
public static bool Flash(Form form)
{
var fi = Create_FLASHWINFO(form.Handle, FLASHW_ALL | FLASHW_TIMERNOFG, uint.MaxValue, 0);
return PInvoke.FlashWindowEx(ref fi);
}
internal static class PInvoke
{
[DllImport("DwmApi")]
internal static extern int DwmSetWindowAttribute(IntPtr hwnd, int dwAttribute, int[] pvAttribute, int cbAttribute);
[DllImport("DwmApi")]
internal static extern int DwmGetWindowAttribute(IntPtr hwnd, int dwAttribute, IntPtr pvAttribute, int cbAttribute);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
internal static extern int SetWindowLong(IntPtr hwnd, int index, int newStyle);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool FlashWindowEx(ref FLASHWINFO pwfi);
}
[StructLayout(LayoutKind.Sequential)]
internal struct FLASHWINFO
{
/// <summary>
/// The size of the structure in bytes.
/// </summary>
public uint cbSize;
/// <summary>
/// A Handle to the Window to be Flashed. The window can be either opened or minimized.
/// </summary>
public IntPtr hwnd;
/// <summary>
/// The Flash Status.
/// </summary>
public uint dwFlags;
/// <summary>
/// The number of times to Flash the window.
/// </summary>
public uint uCount;
/// <summary>
/// The rate at which the Window is to be flashed, in milliseconds. If Zero, the function uses the default cursor blink
/// rate.
/// </summary>
public uint dwTimeout;
}
}
}