MicrosoftDocs / sysinternals

Content for sysinternals.com
http://sysinternals.com
Creative Commons Attribution 4.0 International
454 stars 255 forks source link

[Feature] RDCMAN - add Darkmode #787

Open xperia-droid opened 4 months ago

xperia-droid commented 4 months ago

Please add an dark mode in RDCMAN. I made an approach by myself, to change the colors in the main form: RDCMAN

I didn't change the contextmenus and the dialog forms, only the main window.

I have set the darkmode colors via DNSPY. The following changes were made:

Class Mainform

//Added colors add the start
private MainForm()
{
    this.BackColor = Color.FromArgb(255, 45, 45, 48);
    this.ForeColor = Color.FromArgb(255, 241, 241, 241);
    ...
}

//Replace function
public void AddToClientPanel(Control client)
{
    client.BackColor = Color.FromArgb(255, 45, 45, 48);
    client.ForeColor = Color.FromArgb(255, 241, 241, 241);
    this._clientPanel.Controls.Add(client);
}

Class FormTools

//Fix for centering the options menu
public static void ScaleAndLayout(this Form form)
{
    form.PerformAutoScale();
    form.StartPosition = FormStartPosition.CenterParent;
    form.ResumeLayout(false);
    form.PerformLayout();
}

Class ServerTree

//Change local variables
private static readonly Color NotFocusedForeColor = Color.FromKnownColor(KnownColor.ControlDark);
private static readonly Color NotFocusedBackColor = Color.FromArgb(255, 30, 30, 30);
private static readonly Color FocusedForeColor = Color.FromArgb(255, 241, 241, 241);
private static readonly Color FocusedBackColor = Color.FromArgb(255, 37, 37, 38);

Class RdcBaseForm

//Change menustrip coloring + rendermode
protected RdcBaseForm()
{
    base.AutoScaleDimensions = new SizeF(96f, 96f);
    base.AutoScaleMode = AutoScaleMode.Dpi;
    this._menuPanel = new Panel
    {
        Dock = DockStyle.None
    };
    this._menuStrip = new RdcMenuStrip
    {
        BackColor = Color.FromArgb(255, 45, 45, 48),
        ForeColor = Color.FromArgb(255, 241, 241, 241),
        RenderMode = ToolStripRenderMode.System,
        Visible = true
    };
    this._menuStrip.MenuActivate += delegate(object s, EventArgs e)
    {
        this.SetMainMenuVisibility(true);
        this.UpdateMainMenu();
    };
    this._menuPanel.Controls.Add(this._menuStrip);
    base.Controls.Add(this._menuPanel);
}
Simke771 commented 1 week ago

May I ask how were you able to change RDCMAN.exe? I did download DNSPY, and opened RDCMAN.exe, then navigated to RdcMan --> MainForm, but I have no idea how to add those lines of code that you wrote above. Sorry for the noob question, but I'm not familiar with coding etc.

xperia-droid commented 1 week ago

Hi @Simke771,

your first steps were so far correct.

On the first function you right click inside the "Function private MainForm() and then click on "Edit method" in the contextmenu. There you can change The code as you like.

After you have finished the modifications on the Function you click on button "Compile" on the bottom right. If the changed code is correct, the modification window disappears and you can see the new code in the DNSPY tab.

When You have made all the changes you wanted. You go in the Menu on File -> Save Module. In the New Window you can set an new name in the path (example: RDCMAN_dark.exe) and click on OK to save the file.

Simke771 commented 1 week ago

Thank you @xperia-droid for your fast reply, but it seems like I'm still doing something wrong. Even when I just open Edit Method, and don't change anything, Complie will fail with a bunch of errors. Adding your code to it, just increases number of errors. Hopefully you can point me in the right direction.

image

Also not sure if I'm adding your code correctly, but like I said, my first problem is that compile doesn't work even when I do zero changes to it.

Original looks like this:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Runtime.CompilerServices;
using System.Windows.Forms;

namespace RdcMan
{
    // Token: 0x02000067 RID: 103
    internal partial class MainForm : RdcBaseForm, IMainForm
    {
        // Token: 0x060002A1 RID: 673 RVA: 0x0000F300 File Offset: 0x0000D500
        private MainForm()
        {
            Dictionary<Keys, Action> dictionary = new Dictionary<Keys, Action>();
            dictionary.Add((Keys)131150, delegate
            {
                this.OnFileNew();
            });
            dictionary.Add((Keys)131151, delegate
            {
                this.OnFileOpen();
            });
            dictionary.Add((Keys)131155, delegate
            {
                this.OnFileSave();
            });
            dictionary.Add((Keys)131137, delegate
            {
                AddNodeDialogHelper.AddServersDialog();
            });
            dictionary.Add((Keys)131143, delegate
            {
                AddNodeDialogHelper.AddGroupDialog();
            });
            dictionary.Add((Keys)131142, delegate
            {
                MenuHelper.FindServers();
            });
            dictionary.Add((Keys)131153, delegate
            {
                MenuHelper.ConnectTo();
            });
            this.Shortcuts = dictionary;
            Dictionary<Keys, Action<RdcTreeNode>> dictionary2 = new Dictionary<Keys, Action<RdcTreeNode>>();
            dictionary2.Add(Keys.Delete, delegate(RdcTreeNode node)
            {
                ServerTree.Instance.ConfirmRemove(node, true);
            });
            dictionary2.Add(Keys.RButton | Keys.MButton | Keys.Back | Keys.Space | Keys.Shift, delegate(RdcTreeNode node)
            {
                ServerTree.Instance.ConfirmRemove(node, false);
            });
            dictionary2.Add(Keys.Return, delegate(RdcTreeNode node)
            {
                node.Connect();
            });
            dictionary2.Add(Keys.LButton | Keys.MButton | Keys.Back | Keys.Shift, delegate(RdcTreeNode node)
            {
                ServerBase serverBase = node as ServerBase;
                bool isConnected;
                if (serverBase != null)
                {
                    isConnected = serverBase.IsConnected;
                }
                else
                {
                    bool flag;
                    (node as GroupBase).AnyOrAllConnected(out flag, out isConnected);
                }
                if (!isConnected)
                {
                    node.DoConnectAs();
                }
            });
            dictionary2.Add(Keys.LButton | Keys.MButton | Keys.Back | Keys.Alt, delegate(RdcTreeNode node)
            {
                if (node.HasProperties)
                {
                    node.DoPropertiesDialog();
                }
            });
            dictionary2.Add((Keys)131140, delegate(RdcTreeNode node)
            {
                MenuHelper.AddFavorite(node);
            });
            this.SelectedNodeShortcuts = dictionary2;
        }
    }
}

If I understand your original post it should be like this:


using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Runtime.CompilerServices;
using System.Windows.Forms;

namespace RdcMan
{
    // Token: 0x02000067 RID: 103
    internal partial class MainForm : RdcBaseForm, IMainForm
    {
        // Token: 0x060002A1 RID: 673 RVA: 0x0000F300 File Offset: 0x0000D500
        private MainForm()
        {
            this.BackColor = Color.FromArgb(255, 45, 45, 48);
            this.ForeColor = Color.FromArgb(255, 241, 241, 241);
            Dictionary<Keys, Action> dictionary = new Dictionary<Keys, Action>();
            dictionary.Add((Keys)131150, delegate
            {
                this.OnFileNew();
            });
            dictionary.Add((Keys)131151, delegate
            {
                this.OnFileOpen();
            });
            dictionary.Add((Keys)131155, delegate
            {
                this.OnFileSave();
            });
            dictionary.Add((Keys)131137, delegate
            {
                AddNodeDialogHelper.AddServersDialog();
            });
            dictionary.Add((Keys)131143, delegate
            {
                AddNodeDialogHelper.AddGroupDialog();
            });
            dictionary.Add((Keys)131142, delegate
            {
                MenuHelper.FindServers();
            });
            dictionary.Add((Keys)131153, delegate
            {
                MenuHelper.ConnectTo();
            });
            this.Shortcuts = dictionary;
            Dictionary<Keys, Action<RdcTreeNode>> dictionary2 = new Dictionary<Keys, Action<RdcTreeNode>>();
            dictionary2.Add(Keys.Delete, delegate(RdcTreeNode node)
            {
                ServerTree.Instance.ConfirmRemove(node, true);
            });
            dictionary2.Add(Keys.RButton | Keys.MButton | Keys.Back | Keys.Space | Keys.Shift, delegate(RdcTreeNode node)
            {
                ServerTree.Instance.ConfirmRemove(node, false);
            });
            dictionary2.Add(Keys.Return, delegate(RdcTreeNode node)
            {
                node.Connect();
            });
            dictionary2.Add(Keys.LButton | Keys.MButton | Keys.Back | Keys.Shift, delegate(RdcTreeNode node)
            {
                ServerBase serverBase = node as ServerBase;
                bool isConnected;
                if (serverBase != null)
                {
                    isConnected = serverBase.IsConnected;
                }
                else
                {
                    bool flag;
                    (node as GroupBase).AnyOrAllConnected(out flag, out isConnected);
                }
                if (!isConnected)
                {
                    node.DoConnectAs();
                }
            });
            dictionary2.Add(Keys.LButton | Keys.MButton | Keys.Back | Keys.Alt, delegate(RdcTreeNode node)
            {
                if (node.HasProperties)
                {
                    node.DoPropertiesDialog();
                }
            });
            dictionary2.Add((Keys)131140, delegate(RdcTreeNode node)
            {
                MenuHelper.AddFavorite(node);
            });
            this.SelectedNodeShortcuts = dictionary2;
        }

            public void AddToClientPanel(Control client)
            {
                client.BackColor = Color.FromArgb(255, 45, 45, 48);
                client.ForeColor = Color.FromArgb(255, 241, 241, 241);
                this._clientPanel.Controls.Add(client);
            }
    }
}
xperia-droid commented 1 week ago

Hi @Simke771,

interesting behavior, I can't reproduce it on my side. Somehow DNSPY uses .NET Framework 4.5 and .NET 5.0 simultaneously on your side and you get the error that Size exists in 2 variations.

If you send me your mail adress (or another file transfer link) I can send you my compiled executable to you. Because at the moment I don't know how to fix your issue.

An easy idea would be to remove .NET 5.0 from your installed programs, restart DNSPY and try again.

Simke771 commented 1 week ago

Hey @xperia-droid

You are correct, the problem was because I also have .NET 5.0 on my work computer since it was required for some other software that we are using. I tried to compile it on my other computer without .NET 5.0 and it works exactly as it should. Thanks again for your help, I can now finally use RDCMAN in dark mode :)

karynnglli commented 1 day ago

Hello @xperia-droid,

Thank you for this awesome post! Already 99% better and less painful on my eyes.

I was wondering if you could, when time permits, find out where one goes to change the title bar and contextmenus: image

And this dialog forms: image

And for the client area when a "not connected server" is clicked on: image

As well, can this icon (or a better version of it) be used to replace the current to show it is the dark mode version: RDCMan_32512 (dark).zip

Pretty pretty please?

xperia-droid commented 21 hours ago

Hi @karynnglli,

I have read your post and I will go trough the easy steps to the hard ones.

Thumbnail

To set the background of the thumbnail panels, you need to change the method "AddToClientPanel" in the MainForm class:

class MainForm

From

public void AddToClientPanel(Control client)
{
    this._clientPanel.Controls.Add(client);
}

To

public void AddToClientPanel(Control client)
{
    client.BackColor = Color.FromArgb(255, 45, 45, 48);
    client.ForeColor = Color.FromArgb(255, 241, 241, 241);
    this._clientPanel.Controls.Add(client);
}

Then compile the method.

TitleBar

For an dark title bar you have to make some code modifications on the class "RdcBaseForm". Here you have to edit the class from the contextmenu and not edit the method, because we have to add 2 new methods.

Copy this code

[DllImport("DwmApi")] //System.Runtime.InteropServices
private static extern int DwmSetWindowAttribute(IntPtr hwnd, int attr, int[] attrValue, int attrSize);

protected override void OnHandleCreated(EventArgs e)
{
    if (DwmSetWindowAttribute(Handle, 19, new[] { 1 }, 4) != 0)
        DwmSetWindowAttribute(Handle, 20, new[] { 1 }, 4);
}

And insert it under this line:

public abstract partial class RdcBaseForm : Form
    {

Then compile the class.

Source from here: https://stackoverflow.com/a/64927217

Replace Icons

To replace the Icons, we need to replace 2 different icons.

Replace Resource Icon

The Resource Icon is the Icon you see in the Title Bar on the left side.

In DnSpy scroll on the left side up to the top and open the Resources folder. Inside this folder are all internal Resources located.

We have to replace the resource called "RdcMan.Resources.app.ico".

  1. For this to achieve right click on the Resources folder and click on "Create File resource...".
  2. In the new window open your RDCMan_32512 (dark).ico.
  3. Then you will see there is a new resource called "RDCMan_32512 (dark).ico" in the Resources folder.
  4. Now right click on the new icon and click on "Edit Resource....".
  5. Set the name of the Resource to: RdcMan.Resources.app.ico this name is case sensitive (so copy the name is the best approach).
  6. Then you have 2 resources called: RdcMan.Resources.app.ico
  7. click on both resources and you see the new and old icon to appear.
  8. right click on the old icon and click "Delete RdcMan.Resources.app.ico"

Replace Manifest Icon

The Manifest Icon is the Icon you see in the windows explorer. To be able to replace the Manifest Icon, save the current changes in DnSpy in an new executable under File -> Save Module...

You have to save this file, because we need another programm called Resource Hacker to be able to replace the manifest Icon.

After you saved the file, download the good old Resource Hacker from here: https://www.angusj.com/resourcehacker/ (The Downloads are on the bottom of the page) Direct zip file Link from the site: https://www.angusj.com/resourcehacker/resource_hacker.zip

Extract the zip file to an destination you like and open the file ResourceHacker.exe.

In Resource Hacker open the previous saved RDCMAN executable.

On the Left side you have an TreeView structure. RTight click on the folder called "Icon Group". In the context menu click on "Replace Icon ...". In the new window click on the button "Open file with new icon..." and search for your "RDCMan_32512 (dark).ico" and open it.

After you selected the icon click on the button "Replace".

Then in the main window of Resource Hacker click on "File" -> Save.

Now the Icon on the RDCMAN executable has been changed.

Sometimes after you make some changes with Resource Hacker you see the old Icon. The problem here lies with Explorer, as it caches the icons in an IconDB cache file.

To fix it, you can do one of the 2 following solutions:

  1. Rename the executable to RDCMAN1.exe or so. Sometimes you can see now the new icon.
  2. Clear the Icon Cache.

For Clearing the Icon cache follow this process:

  1. Open Command Prompt as administrator from the start menu.
  2. Run taskkill /F /IM explorer.exe to stop explorer, or kill all explorer windows with Task manager.
  3. Run del %localappdata%\Microsoft\Windows\Explorer\iconcache* to delete the icon cache files.
  4. Run start explorer.exe to start explorer and the icon cache should be rebuilt automatically.

Custom ContextMenu and MenuStrips

The elephant in the room is the MenuStrips and Contextmenu part.

The default MenuStrips / ContextMenus doesn't fit an dark theme and therefore are completly unreadable.

As an workaround I set the RenderMode of the MenuStrip to System. Because with this RenderMode you can have an dark Mode in the Title Bar and be able to read the content of the ContextMenus / MenuStrip.

I have made the change in the class RdcBaseForm (RdcBaseForm method) (From my first post).

this._menuStrip = new RdcMenuStrip
    {
        BackColor = Color.FromArgb(255, 45, 45, 48),
        ForeColor = Color.FromArgb(255, 241, 241, 241),
        RenderMode = ToolStripRenderMode.System,
        Visible = true
    };

If you switch the RenderMode to any other Option than System, compile the method, save the module and start it you will see what I mean.

To be able to set an dark mode RenderMode you have to create an new class and then set the RenderMode of the ToolStrip to the new generated class.

I don't know If you can add a new class with DnSpy to the RDCMan executable and then switch the RenderMode I need to test it.

For this part I will add an new post regarding my findings.

xperia-droid commented 19 hours ago

Custom MenuStrip

To create an dark Mode to the MenuStrip we have to add 2 classes: In DnSpy right click on the namespace RdcMan and click on "Add Class (C#)...".

In the new Window paste this code in:

using System;
using System.Drawing;
using System.Windows.Forms;

namespace RdcMan
{
    public class MyColorTable : ProfessionalColorTable
    {
        public override Color ToolStripDropDownBackground => Color.FromArgb(255, 45, 45, 48);

        public override Color ImageMarginGradientBegin => Color.FromArgb(255, 45, 45, 48);
        public override Color ImageMarginGradientMiddle => Color.FromArgb(255, 45, 45, 48);
        public override Color ImageMarginGradientEnd => Color.FromArgb(255, 45, 45, 48);

        public override Color MenuBorder => Color.Black;
        public override Color MenuItemBorder => Color.Black;

        public override Color MenuItemSelected => Color.FromArgb(255, 80, 80, 80);

        public override Color MenuStripGradientBegin => Color.FromArgb(255, 45, 45, 48);
        public override Color MenuStripGradientEnd => Color.FromArgb(255, 45, 45, 48);

        public override Color MenuItemSelectedGradientBegin => Color.FromArgb(255, 51, 51, 52);
        public override Color MenuItemSelectedGradientEnd => Color.FromArgb(255, 45, 45, 48);

        public override Color MenuItemPressedGradientBegin => Color.FromArgb(255, 45, 45, 48);
        public override Color MenuItemPressedGradientEnd => Color.FromArgb(255, 45, 45, 48);
    }
}

Then click on compile.

For the next class to be added we have to save the Module, because the second class references the added class.

So click on File -> "Save Module...". You can save it with the same file name or choose an different executable name.

If you use the same name you click under File -> Reload All Assemblies to geload all files.

If you don't use the same name click under File -> "Close All" and then File -> "Open..." to open the new created file.

The reload / opening of the new file is required to add the new class. Otherwise you get an compile error on the next step.

After the reload right click again on the namespace RdcMan and click on "Add Class (C#)...".

This time you add this class:

using System;
using System.Drawing;
using System.Windows.Forms;

namespace RdcMan
{
    public class MyRenderer : ToolStripProfessionalRenderer
    {
        public MyRenderer() : base(new MyColorTable()) { }

        protected override void OnRenderItemText(ToolStripItemTextRenderEventArgs e)
        {
            e.TextColor = Color.White; // Set the text color to white
            base.OnRenderItemText(e);
        }
    }
}

Then again we have to save the module under File -> "Save Module" and execute the same procedure as on the first class to reload the saved file.

We have to go trough this process again, because we need to reference the second generated class in the RdcBaseForm class.

Open the RdcBaseForm under the RdcMan namespace. Edit the method called "protected RdcBaseForm()" with right click on the method and select "Edit Method"

Replace this part:

            this._menuStrip = new RdcMenuStrip
            {
                BackColor = Color.FromArgb(255, 45, 45, 48),
                ForeColor = Color.FromArgb(255, 241, 241, 241),
                RenderMode = ToolStripRenderMode.System,
                Visible = true
            };

With this

            this._menuStrip = new RdcMenuStrip
            {
                BackColor = Color.FromArgb(255, 45, 45, 48),
                ForeColor = Color.FromArgb(255, 241, 241, 241),
                Renderer = new MyRenderer(),
                Visible = true
            };

compile the method and save the module for the last time. Now you have an darkmode MenuStrip.

ContextMenus

To change the ContextMenus style you have to be added the classes from above.

ServerTree ContextMenus

In DnSpy move to the class ServerTree in the RdcMan namespace.

Then move the the method called Init: "internal void Init(Assembly myAssembly)"

In this function we have to add the Renderer to the contextmenu:

Replace this code:

            ContextMenuStrip contextMenuStrip = new ContextMenuStrip();
            contextMenuStrip.Opening += this.OnContextMenu;
            this.ContextMenuStrip = contextMenuStrip;

with this code:

            ContextMenuStrip contextMenuStrip = new ContextMenuStrip();
            contextMenuStrip.Renderer = new MyRenderer();
            contextMenuStrip.Opening += this.OnContextMenu;
            this.ContextMenuStrip = contextMenuStrip;

Then save the method and afterwards save the module. Now are the contextmenus in the ServerTree region in dark mode.

Thumbnail ContextMenus

When you right click on the Thumbnail of an connection there is another Contextmenu we have to change.

To set the darkmode for this Contextmenu move to the class "ServerLabel". There is the method called "static ServerLabel()" we will replace this method.

Replace:

        static ServerLabel()
        {
            ServerLabel._menu = new ContextMenuStrip();
            ServerLabel._menu.Opening += ServerLabel.MenuPopup;
            Button button = new Button
            {
                FlatStyle = FlatStyle.Flat,
                Font = new Font(ServerTree.Instance.Font, FontStyle.Bold)
            };
            ServerLabel.Height = button.Height;
        }

With:

        static ServerLabel()
        {
            ServerLabel._menu = new ContextMenuStrip();
            ServerLabel._menu.Renderer = new MyRenderer();
            ServerLabel._menu.Opening += ServerLabel.MenuPopup;
            ServerLabel.Height = new Button
            {
                FlatStyle = FlatStyle.Flat,
                Font = new Font(ServerTree.Instance.Font, FontStyle.Bold)
            }.Height;
        }

Save the method and save the Module at the last time under File -> "Save Module...". Now is the main Window of RDCMAN complete in dark Mode.

I have tried in the past to set an dark mode on the different windows in RDCMAN, but the result was very hilarious.

I tried my best to fix it, but after an hour of trying I let it go. Because the most time I'm using RDCMAN is using the RDP connections, so my main goal was it to have an dark mode on the main window. Which should be now fully working.