Manipulate any program by using C#

Manipulate any program by using CsharpThis article is going to be all about how you can control, customize and extend other programs using C#. As an example scenario we’re going to be extending the default Windows program Notepad with a custom function.

The C# application will modify all running instances of Notepad by adding a new button to the UI and listening for clicks on the button. A click on the button will convert the text in the Notepad text field into HTML using a Markdown parser (What is Markdown?) and display the HTML in our application.

If you follow the article, the finished product is going to look like this:

The article will have you construct the program step by step. If you want to read the full source code right away, you can skip directly to the end of the article – the complete C# source code is provided there. A download of the Visual Studio solution is also provided as an alternative.

Manipulating other programs using C#

To begin, let’s create a new WinForms project in Visual Studio. We’ll add a RichTextBox to the standard form, set its name to “richTextBoxInfo” and set the dock attribute to “Fill”. Now the form should look like in the screenshot above.

Now let’s open the Solution Explorer, click the References tab and add two new references using right-click – one to WindowsBase.dll, which can be found under “.NET”, and one to MarkdownSharp.dll under “Browse”. MarkdownSharp.dll can be downloaded from the project website for MarkdownSharp. Once that’s done, we can finally begin writing the actual program.

First of all, we’re going to add three new using references: System.Runtime.InteropServices, System.Diagnostics and MarkdownSharp. We need InteropService to run methods from unmanaged code, System.Diagnostics to list and analyse processes, and Markdown to parse the Markdown text later.

Next we’re going to add a whole load of “unmanaged code” – methods, constants and auxiliary variables. What everything means and does will be explained as the article progresses, therefore for now you can safely copy them from the following code box.

By the way, a quick tip: if you want to know what a native/unmanaged method signature should look like, you can have a look at pinvoke.net.

Now our code should look like this:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Runtime.InteropServices;
using System.Diagnostics;
using MarkdownSharp;

namespace NotepadEnhanced
{
    public partial class Form1 : Form
    {
        [DllImport("user32.dll")]
        private static extern int GetWindowText(IntPtr hWnd, StringBuilder text, int count); 

        [DllImport("user32.dll")]
        private static extern IntPtr GetMenu(IntPtr hWnd);

        [DllImport("user32.dll")]
        private static extern bool InsertMenu(IntPtr hMenu, Int32 wPosition, Int32 wFlags, Int32 wIDNewItem, string lpNewItem);

        [DllImport("user32.dll")]
        private static extern int GetMenuItemCount(IntPtr hMenu);

        [DllImport("user32.dll")]
        private static extern bool DrawMenuBar(IntPtr hWnd);

        [DllImport("user32.dll", SetLastError = true)]
        private static extern IntPtr FindWindowEx(IntPtr hwndParent,
            IntPtr hwndChildAfter, string lpszClass, IntPtr lpszWindow);

        [DllImport("user32.dll", EntryPoint = "SendMessage")]
        private static extern IntPtr SendMessageGetTextW(IntPtr hWnd,
            uint msg, UIntPtr wParam, StringBuilder lParam);

        [DllImport("User32.dll", EntryPoint = "SendMessage")]
        private extern static int SendMessageGetTextLength(IntPtr hWnd,
            int msg, IntPtr wParam, IntPtr lParam);

        [DllImport("user32.dll")]
        [return: MarshalAs(UnmanagedType.Bool)]
        private static extern bool SetForegroundWindow(IntPtr hWnd);

        [DllImport("user32.dll")]
        static extern IntPtr SetWinEventHook(uint eventMin, uint eventMax, IntPtr
           hmodWinEventProc, WinEventDelegate lpfnWinEventProc, uint idProcess,
           uint idThread, uint dwFlags);

        [DllImport("user32.dll")]
        static extern bool UnhookWinEvent(IntPtr hWinEventHook);

        delegate void WinEventDelegate(IntPtr hWinEventHook, uint eventType,
                      IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime);

        WinEventDelegate procDelegate;
        IntPtr hhook;

        const uint EVENT_OBJECT_INVOKED = 0x8013;
        const uint WINEVENT_OUTOFCONTEXT = 0;
        private const Int32 _CustomMenuID = 1000;
        private const Int32 MF_BYPOSITION = 0x400;
        private const Int32 WM_SYSCOMMAND = 0x112;

        List<IntPtr> handlesNp = new List<IntPtr>();
        Dictionary<IntPtr, IntPtr> handlesNpEdit = new Dictionary<IntPtr, IntPtr>();

        public Form1()
        {
            InitializeComponent();
        }
    }
}

Identifying processes and selecting handles

Now let’s set up an event handler for the load event of our main form (Form1). You can do it by hand or using the Visual Studio Designer, it doesn’t matter.

Inside the load method we request all running Notepad instances from the system and save them in a list – we do have to know which program to manipulate.

Now we go through the list with all the Notepad instances. Here, we first get the text in the window title and pass it to richTextBoxInfo in our main form.

Afterwards we make a reference to the text field of the respective Notepad window. We need it to gain access to the text contained in it later.

Finally we check how many buttons the Notepad window has in the menu bar. If it has fewer than 6 buttons then it hasn’t been manipulated yet, so we can add our button.

Once we’ve gone through the list of Notepad instances, we need to set up another EventHook. This one will listen for Windows events.

The code of the load method looks like this. (The comments in the code should tell you what line does what in case you forget.)

private void Form1_Load(object sender, EventArgs e)
{
    //Get the window-handles of all Notepad processes
    handlesNp = Process.GetProcessesByName("notepad").Select(x => x.MainWindowHandle).ToList();

    //Check each Notepad instance
    richTextBoxInfo.Text = "An folgende Fenster angedockt:rn";
    foreach (var npHandle in handlesNp.Select((x, i) => new {Handle = x, Index = i}))
    {
        //Print the name of the actual Notepad instance
        StringBuilder sb = new StringBuilder();
        GetWindowText(npHandle.Handle, sb, 200);
        var windowTitle = sb.ToString();
        richTextBoxInfo.Text += windowTitle + "rn";

        //Get the handle of the textbox from the actual Notepad instance
        //and save it for later use
        handlesNpEdit.Add(npHandle.Handle, FindWindowEx(npHandle.Handle, IntPtr.Zero, "Edit", IntPtr.Zero));

        //Get the menu-handle
        var sysMenu = GetMenu(npHandle.Handle);
        //If the menu has no "extra" button, add it
        //and repaint the menu
        if (GetMenuItemCount(sysMenu) != 6)
        {
            InsertMenu(sysMenu, 4, MF_BYPOSITION, _CustomMenuID, "MD zu HTML");
            DrawMenuBar(npHandle.Handle);
        }
    }

    //Set a hook, to catch Windows system messages
    procDelegate = new WinEventDelegate(WinEventProc);
    hhook = SetWinEventHook(EVENT_OBJECT_INVOKED, EVENT_OBJECT_INVOKED, IntPtr.Zero,
            procDelegate, 0, 0, WINEVENT_OUTOFCONTEXT);
}

If we try to compile the program now, Visual Studio will throw us an error message. The reason for that is this line of code that we wrote at the end of the load method.

procDelegate = new WinEventDelegate(WinEventProc);

Here we pass a method called “WinEventProc” as an argument – a method that doesn’t exist in our code yet. Therefore, our next step will be to create this method and make it do things.

Listening for Windows events with C#

Since this method catches lots and lots of Windows events, and since we only need those associated with the user clicking on our newly added button, we need to filter the relevant events using an if statement. To achieve this, we need to check if the event was triggered by a Notepad instance contained in the list we created in the load method (handlesNp.Contains(hwnd)) and if it was triggered by our button (_CustomMenuID == idChild).

If the event fits both criteria, we get the text out of the Notepad text field using the GetWindowText() method. Afterwards we set up an instance of Markdown and convert the Notepad text from Markdown to HTML.

Now we create a new window/form, add a WebBrowser and a RichTextBox to it and pass our freshly parsed HTML to both elements.

Finally, we open the new window and move it to the foreground. The code for all that looks like this:

void WinEventProc(IntPtr hWinEventHook, uint eventType,
       IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime)
{
    //Check if it's a message from Notepad and if the message
    //was send by our custom button
    if (handlesNp.Contains(hwnd) && _CustomMenuID == idChild)
    {
        //Get the text(-value) from Notepad
        var markdownText = GetWindowText(handlesNpEdit[hwnd]);

        //Render the input text as markdown
        Markdown mds = new Markdown();
        var htmlOutput = htmlBody.Replace("{html_body}", mds.Transform(markdownText));

        //Create new window
        Form fWeb = new Form();
        fWeb.Size = new System.Drawing.Size(800, 600);
        fWeb.StartPosition = FormStartPosition.CenterScreen;
        fWeb.Text = "Notepad - Markdown Extension";

        //Add a webbrowser to the window and
        WebBrowser wb = new WebBrowser();
        wb.Dock = DockStyle.Fill;
        wb.ScriptErrorsSuppressed = true;
        wb.DocumentText = htmlOutput;

        //Add a textbox
        RichTextBox rtHtml = new RichTextBox();
        rtHtml.Dock = DockStyle.Fill;
        rtHtml.Text = htmlOutput;

        //Place webbrowser and textbox side-by-side
        TableLayoutPanel tlp = new TableLayoutPanel();
        tlp.Dock = DockStyle.Fill;
        tlp.ColumnCount = 1;
        tlp.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F));
        tlp.RowCount = 2;
        tlp.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 50F));
        tlp.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 50F));
        tlp.Controls.AddRange(new Control[] { wb, rtHtml });
        fWeb.Controls.Add(tlp);

        //Show the windows
        fWeb.Show();
        SetForegroundWindow(fWeb.Handle);
    }

}

We press F5, the compiler runs, and… error! We have the hook method this time, but we used a new method in it that doesn’t exist yet. Therefore we add the definition of the missing GetWindowText() method.

public static string GetWindowText(IntPtr hwnd)
{
   int len = SendMessageGetTextLength(hwnd, 14, IntPtr.Zero, IntPtr.Zero) + 1;
   StringBuilder sb = new StringBuilder(len);
   SendMessageGetTextW(hwnd, 13, new UIntPtr((uint)len), sb);
   return sb.ToString();
}

A variable is missing as well – the htmlBody string variable. It contains the HTML and CSS code that displays our text in the WebBrowser element. (This is somewhat long, so I collapsed the following code block. A click on the code box will expand it.)

#region Markdown / CSS-Code
string htmlBody = @"
  <!DOCTYPE html PUBLIC ""-//W3C//DTD XHTML 1.0 Transitional//EN"" ""http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"">
  <html xmlns=""http://www.w3.org/1999/xhtml"" xml:lang=""en"" lang=""en"">
  <head>
      <style type=""text/css"">;
          body {
          font-family: Helvetica, arial, sans-serif;
          font-size: 14px;
          line-height: 1.6;
          padding-top: 10px;
          padding-bottom: 10px;
          background-color: white;
          padding: 30px; }

        body > *:first-child {
          margin-top: 0 !important; }
        body > *:last-child {
          margin-bottom: 0 !important; }

        a {
          color: #4183C4; }
        a.absent {
          color: #cc0000; }
        a.anchor {
          display: block;
          padding-left: 30px;
          margin-left: -30px;
          cursor: pointer;
          position: absolute;
          top: 0;
          left: 0;
          bottom: 0; }

        h1, h2, h3, h4, h5, h6 {
          margin: 20px 0 10px;
          padding: 0;
          font-weight: bold;
          -webkit-font-smoothing: antialiased;
          cursor: text;
          position: relative; }

        h1:hover a.anchor, h2:hover a.anchor, h3:hover a.anchor, h4:hover a.anchor, h5:hover a.anchor, h6:hover a.anchor {
          background: url('../../images/modules/styleguide/para.pn') no-repeat 10px center;
          text-decoration: none; }

        h1 tt, h1 code {
          font-size: inherit; }

        h2 tt, h2 code {
          font-size: inherit; }

        h3 tt, h3 code {
          font-size: inherit; }

        h4 tt, h4 code {
          font-size: inherit; }

        h5 tt, h5 code {
          font-size: inherit; }

        h6 tt, h6 code {
          font-size: inherit; }

        h1 {
          font-size: 28px;
          color: black; }

        h2 {
          font-size: 24px;
          border-bottom: 1px solid #cccccc;
          color: black; }

        h3 {
          font-size: 18px; }

        h4 {
          font-size: 16px; }

        h5 {
          font-size: 14px; }

        h6 {
          color: #777777;
          font-size: 14px; }

        p, blockquote, ul, ol, dl, li, table, pre {
          margin: 15px 0; }

        hr {
          background: transparent url('../../images/modules/pulls/dirty-shade.png') repeat-x 0 0;
          border: 0 none;
          color: #cccccc;
          height: 4px;
          padding: 0; }

        body > h2:first-child {
          margin-top: 0;
          padding-top: 0; }
        body > h1:first-child {
          margin-top: 0;
          padding-top: 0; }
          body > h1:first-child + h2 {
            margin-top: 0;
            padding-top: 0; }
        body > h3:first-child, body > h4:first-child, body > h5:first-child, body > h6:first-child {
          margin-top: 0;
          padding-top: 0; }

        a:first-child h1, a:first-child h2, a:first-child h3, a:first-child h4, a:first-child h5, a:first-child h6 {
          margin-top: 0;
          padding-top: 0; }

        h1 p, h2 p, h3 p, h4 p, h5 p, h6 p {
          margin-top: 0; }

        li p.first {
          display: inline-block; }

        ul, ol {
          padding-left: 30px; }

        ul :first-child, ol :first-child {
          margin-top: 0; }

        ul :last-child, ol :last-child {
          margin-bottom: 0; }

        dl {
          padding: 0; }
          dl dt {
            font-size: 14px;
            font-weight: bold;
            font-style: italic;
            padding: 0;
            margin: 15px 0 5px; }
            dl dt:first-child {
              padding: 0; }
            dl dt > :first-child {
              margin-top: 0; }
            dl dt > :last-child {
              margin-bottom: 0; }
          dl dd {
            margin: 0 0 15px;
            padding: 0 15px; }
            dl dd > :first-child {
              margin-top: 0; }
            dl dd > :last-child {
              margin-bottom: 0; }

        blockquote {
          border-left: 4px solid #dddddd;
          padding: 0 15px;
          color: #777777; }
          blockquote > :first-child {
            margin-top: 0; }
          blockquote > :last-child {
            margin-bottom: 0; }

        table {
          padding: 0; }
          table tr {
            border-top: 1px solid #cccccc;
            background-color: white;
            margin: 0;
            padding: 0; }
            table tr:nth-child(2n) {
              background-color: #f8f8f8; }
            table tr th {
              font-weight: bold;
              border: 1px solid #cccccc;
              text-align: left;
              margin: 0;
              padding: 6px 13px; }
            table tr td {
              border: 1px solid #cccccc;
              text-align: left;
              margin: 0;
              padding: 6px 13px; }
            table tr th :first-child, table tr td :first-child {
              margin-top: 0; }
            table tr th :last-child, table tr td :last-child {
              margin-bottom: 0; }

        img {
          max-width: 100%; }

        span.frame {
          display: block;
          overflow: hidden; }
          span.frame > span {
            border: 1px solid #dddddd;
            display: block;
            float: left;
            overflow: hidden;
            margin: 13px 0 0;
            padding: 7px;
            width: auto; }
          span.frame span img {
            display: block;
            float: left; }
          span.frame span span {
            clear: both;
            color: #333333;
            display: block;
            padding: 5px 0 0; }
        span.align-center {
          display: block;
          overflow: hidden;
          clear: both; }
          span.align-center > span {
            display: block;
            overflow: hidden;
            margin: 13px auto 0;
            text-align: center; }
          span.align-center span img {
            margin: 0 auto;
            text-align: center; }
        span.align-right {
          display: block;
          overflow: hidden;
          clear: both; }
          span.align-right > span {
            display: block;
            overflow: hidden;
            margin: 13px 0 0;
            text-align: right; }
          span.align-right span img {
            margin: 0;
            text-align: right; }
        span.float-left {
          display: block;
          margin-right: 13px;
          overflow: hidden;
          float: left; }
          span.float-left span {
            margin: 13px 0 0; }
        span.float-right {
          display: block;
          margin-left: 13px;
          overflow: hidden;
          float: right; }
          span.float-right > span {
            display: block;
            overflow: hidden;
            margin: 13px auto 0;
            text-align: right; }

        code, tt {
          margin: 0 2px;
          padding: 0 5px;
          white-space: nowrap;
          border: 1px solid #eaeaea;
          background-color: #f8f8f8;
          border-radius: 3px; }

        pre code {
          margin: 0;
          padding: 0;
          white-space: pre;
          border: none;
          background: transparent; }

        .highlight pre {
          background-color: #f8f8f8;
          border: 1px solid #cccccc;
          font-size: 13px;
          line-height: 19px;
          overflow: auto;
          padding: 6px 10px;
          border-radius: 3px; }

        pre {
          background-color: #f8f8f8;
          border: 1px solid #cccccc;
          font-size: 13px;
          line-height: 19px;
          overflow: auto;
          padding: 6px 10px;
          border-radius: 3px; }
          pre code, pre tt {
            background-color: transparent;
            border: none; }
    </style>
<title>$_</title>
<meta http-equiv=""Content-Type"" content=""text/html; charset=utf-8"" />
</head>
<body>
{html_body}
</body>
</html>";
#endregion

Now we’re almost done. The last step will be adding a form closing event handler to the form. This event handler will take care of removing our Windows event listener when the program is closed.

private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
	//Remove message hook
	UnhookWinEvent(hhook);
}

Now the program is complete. To test it, we open a Notepad window and write a couple of lines of text in Markdown format. Now we compile and start our program. As soon as its window appears, a new button should show up in the menu bar of the Notepad window.

Funktionsweise - Notepad mit C# um Markdown erweitern

Clicking the button should open another window that shows our Markdown text formatted and as raw HTML code.

Complete source code and download

If this is all too confusing for you or if you don’t have time, you can download the entirety of the source code as well as the complete Visual Studio project by following this link.

Download: Visual Studio example project

 

 

Questions, ideas and feedback

That’s it for today. If you have questions, ideas or feedback about this article, just leave a comment. Also let me know if you have ideas for new topics to write about.

Leave a comment

Please be polite. We appreciate that. Your email address will not be published and required fields are marked