In folgendem Artikel soll es darum gehen, wie man mittels C# andere Programme ansteuern, anpassen und erweitern kann. Als Beispielszenario wollen wir mittels einer kleinen C#-Applikation das standardmäßig mit Windows ausgelieferte Programm “Notepad” um eine Zusatzfunktion erweitern.
Konkret soll sich die C#-Anwendung in alle geöffneten Notepad Instanzen einklinken, die Oberfläche um einen neuen Button erweitern und Klicks auf diesen Button abfangen. Bei einem Klick auf den Button soll der Text aus dem Notepad-Textfeld an unsere Anwendung geschickt und mittels einem Markdown Parser (Was ist Markdown?) in HTML umgewandelt werden.
Das fertige Endprodukt unseres Artikels soll dann wie in dem nachfolgenden Video aussehen, dass ich für euch erstellt habe.
Im Rahmen des Artikel werden wir das Programm Schritt-für-Schritt aufbauen. Wer gleich den kompletten Quelltext haben will, scrollt direkt bis ans Ende des Artikels. Dort ist der komplette C#-Code dargestellt. Alternativ steht auch ein Download der Visual Studio Solution bereit.
Andere Programme per C# manipulieren
Zu Beginn legen wir eine neues WinForms-Projekt in Visual Studio an. Im GUI-Designer fügen wir dem Standard-Form eine RichTextBox hinzu, benennen diese als “richTextBoxInfo” und setzen das Dock-Attribut auf “Fill”. Nun sollte das Formular wie in nebenstehendem Screenshot aussehen.
Im nächsten Schritt fügen wir im Projektmappen-Explorer unter Verweise mittels Rechtsklick zwei neue Verweise hinzu. Einen Verweis auf die WindowsBase.dll, welche über den “.NET”-Reiter aufzufinden ist und einen Verweis auf “MarkdownSharp.dll” über den “Durchsuchen”-Reiter. Die MarkdownSharp.dll kann von der MarkdownSharp-Projektseite heruntergeladen werden. Ist dies geschehen, können wir mit dem Ausprogrammieren des Programms beginnen.
Im ersten Schritt fügen wir drei neue using-Referenzen hinzu: System.Runtime.InteropServices, System.Diagnostics und MarkdownSharp. InteropService benötigen wir um Methoden aus nicht-gemanagetem (unmanaged) Code aufzurufen, System.Diagnostics brauchen wir um Prozesse auflisten und auswerten zu können und Markdown benötigen wir, um später den Markdown-Text parsen zu können.
Im zweiten Schritt fügen wir eine ganze Latte an “unmanaged Code”-Methoden, Konstanten und Hilfsvariablen hinzu. Welcher Part wozu dient, wird sich im Laufe des Artikels klären. Deshalb können wir diese vorerst getrost aus nachfolgender Code-Box kopieren.
Kleiner Tipp am Rande: Wer sich fragt, woher man weiß, wie so eine native/unmanaged Methodensignatur aussehen muss, der kann mal einen Blick auf pinvoke.net werfen.
Unser Code sollte nun wie folgt aussehen:
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(); } } }
Prozesse identifizieren und Handles auslesen
Nun legen wir einen Eventhandler für das Load-Event unseres Hauptformulars (Form1) an. Ob ihr das von Hand oder über den Visual Studio Designer macht, spielt dabei keine Rolle.
Innerhalb der Load-Methode rufen wir beim System alle Notepad-Prozesse ab und speichern diese in einer Liste. Schließlich müssen wir die Programmfenster ja kennen, wenn wir sie erweitern wollen.
Danach durchlaufen wir die Liste mit allen Notepad-Fenster-Instanzen. Hierbei fragen wir zuerst den Text der im Fenster-Titel steht ab und geben diesen in der richTextBoxInfo auf unserem Hauptformular aus.
Danach erfragen wir einen Verweis auf das Textfeld des jeweiligen Notepad-Fensters. Diesen brauchen wir, um später Zugriff auf den Text aus dem Notepad-Fenster zu bekommen.
Abschließend überprüfen wir, wie viel Buttons das Notepad-Fenster in der Menüleiste hat. Wenn es weniger als 6 Buttons hat, dann wurde es noch nicht manipuliert. In diesem Fall fügen wir unseren Button hinzu.
Ist die Liste mit allen Notepad-Fenstern durchlaufen, setzen wir noch einen EventHook. Hierdurch klinken wir uns in System ein und können Windows-Nachrichten abfangen.
Der Code in der Load-Methode sieht wie folgt aus. (Anhand der Kommentare könnt ihr auch noch einmal nachvollziehen, was in den einzelnen Schritten passiert.)
private void Form1_Load(object sender, EventArgs e) { //Die Fenster-Handles aller Notepad-Prozesse abfragen und speichern handlesNp = Process.GetProcessesByName("notepad").Select(x => x.MainWindowHandle).ToList(); //Durchlaufe alle gefundenen Notepad-Instanzen richTextBoxInfo.Text = "An folgende Fenster angedockt:\r\n"; foreach (var npHandle in handlesNp.Select((x, i) => new {Handle = x, Index = i})) { //Gebe den Namen der aktuellen Notepad-Instanz aus StringBuilder sb = new StringBuilder(); GetWindowText(npHandle.Handle, sb, 200); var windowTitle = sb.ToString(); richTextBoxInfo.Text += windowTitle + "\r\n"; //Ermittle das Handles des Textfeldes der aktuellen Notepad-Instanz //und speichere es, um später den Text auszulesen handlesNpEdit.Add(npHandle.Handle, FindWindowEx(npHandle.Handle, IntPtr.Zero, "Edit", IntPtr.Zero)); //Ermittle Handles des Menüs der aktuellen Notepad-Instanz var sysMenu = GetMenu(npHandle.Handle); //Wenn das Menü noch keinen Zusatzbutton bekommen hat, //füge den neuen Button hinzu und zeichne Menü neu if (GetMenuItemCount(sysMenu) != 6) { InsertMenu(sysMenu, 4, MF_BYPOSITION, _CustomMenuID, "MD zu HTML"); DrawMenuBar(npHandle.Handle); } } //Hook setzen, um Windows-Nachrichten abzufangen procDelegate = new WinEventDelegate(WinEventProc); hhook = SetWinEventHook(EVENT_OBJECT_INVOKED, EVENT_OBJECT_INVOKED, IntPtr.Zero, procDelegate, 0, 0, WINEVENT_OUTOFCONTEXT); }
Wenn wir nun versuchen das Programm zu kompilieren, dann dürfte uns Visual Studio eine nette Fehlermeldung um die Ohren werfen. Dies liegt an der folgenden Zeile Code, die wir am Ende der Load-Methode geschrieben haben.
procDelegate = new WinEventDelegate(WinEventProc);
Hier übergeben wir als Parameter eine Methode namens “WinEventProc”, die in unserem Programm jedoch noch gar nicht existiert. Deshalb legen wir nun im nächsten Schritt eben diese Methode an und befüllen sie mit “Leben”.
Windows-Nachrichten mit C# abfangen
Da diese Methode eine ganze Menge an Windows-Nachrichten abfängt, wir jedoch nur auf solche eingehen wollen, die durch den Klick auf unseren neu hinzugefügten Button in Notepad ausgelöst wurden, filtern wir zuerst mittels eine if-Abfrage nur die relevanten Nachrichten heraus. Hierzu überprüfen wir erstens, ob die Nachricht von einer der Notepad-Instanzen kommt, die wir in der Load-Methode in unsere Liste geschrieben haben (handlesNp.Contains(hwnd)) und zweitens, ob die Nachricht von unserem Button ausgelöst wurde (_CustomMenuID == idChild).
Wenn dem so ist, holen wir mittels der GetWindowText()-Methode den Text aus dem Notepad-Textfeld. Danach erstellen wir eine Markdown-Instanz und wandeln den Notepad-Text von Markdown in HTML.
Nun erstellen wir ein neues Fenster/Formular, platzieren einen Webbrowser sowie eine Richtextbox darauf und übergeben beiden unser soeben frisch geparstes HTML.
Abschließend öffnen wir das neue Fenster und holen es in den Vordergrund. Der Code für all das sieht wie folgt aus:
void WinEventProc(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime) { //Überprüfe ob Nachricht von Notepad kommt und durch unseren //eigenen Button ausgelöst wurde. if (handlesNp.Contains(hwnd) && _CustomMenuID == idChild) { //Hole Text aus Notepad-Textfeld var markdownText = GetWindowText(handlesNpEdit[hwnd]); //Rende Text von Markdown nach HTML Markdown mds = new Markdown(); var htmlOutput = htmlBody.Replace("{html_body}", mds.Transform(markdownText)); //Erstelle neues Fenster/Formular Form fWeb = new Form(); fWeb.Size = new System.Drawing.Size(800, 600); fWeb.StartPosition = FormStartPosition.CenterScreen; fWeb.Text = "Notepad - Markdown Extension"; //Füge dem Fenster einen Webbrowser hinzu und lade //HTML in den Browser WebBrowser wb = new WebBrowser(); wb.Dock = DockStyle.Fill; wb.ScriptErrorsSuppressed = true; wb.DocumentText = htmlOutput; //Füge dem Fenster eine Textbox hinzu und lade //HTML in die Textbox RichTextBox rtHtml = new RichTextBox(); rtHtml.Dock = DockStyle.Fill; rtHtml.Text = htmlOutput; //Order Browser und Textbox 50/50 im Fenster an 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); //Fenster anzeigen fWeb.Show(); SetForegroundWindow(fWeb.Handle); } }
Ein Klick auf F5, der Kompiler läuft and und … Fehler! Diesmal haben wir zwar die Hook-Methode, dafür haben wir soeben eine neue Methode verwendet, die noch nicht existiert. Deshalb fügen wir nun noch die fehlende GetWindowText()-Implementierung hinzu.
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(); }
Eine Variable fehlt auch noch. Und zwar der htmlBody-String. Dieser enthält HTML- und CSS-Code zur Darstellung unseres Textes in dem Webbrowser-Element. (Da der Code etwas länger ist, habe ich unten stehenden Codeblock zusammengeklappt. Ein Klick auf die Code-Box hilft weiter.)
#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
Jetzt sind wir fast fertig. Im letzten Schritt fügen wir unserem Formular noch den “Form-Closing”-Eventhandler hinzu. Innerhalb dieses Eventhandlers stellen wir sicher, dass der von uns gesetzte Hook zum Abfangen der Windows-Nachrichten beim Schließen des Programms wieder entfernt wird.
private void Form1_FormClosing(object sender, FormClosingEventArgs e) { //Nachrichten-Hook entfernen UnhookWinEvent(hhook); }
Nun ist alles komplett. Zum Testen öffnen wir Notepad und schreiben ein paar Zeilen Text in Markdown-Formatierung. Nun kompilieren und starten wir unser Programm. Sobald das Fenster erscheint, sollte in Notepad ein neuer Button in der Menüleiste erscheinen.
Ein Klick auf den Button sollte ein weiteres Fenster öffnen, in dem unser Markdown-Text einmal gerendert und einmal als reines HTML dargestellt wird.
Kompletter Sourcecode und Download
Wem das Ganze zu verwirrend war oder wer keine Zeit hat, der findet nachfolgend noch einmal den gesamten Quellcode als komplettes Visual Studio Projekt zum Download.
Download: Visual Studio Beispielprojekt
Fragen, Ideen & Kritik
Damit sind wir für heute auch schon wieder durch. Solltet ihr noch Fragen, Anregungen oder Kritik haben, dann schreib mit einfach einen Kommentar.
Wenn ihr Wünsche für weitere Themen bzw. Artikel habt, dann lasst mir diese einfach zukommen.
Maybe in the future it’ll do even better in those areas, but for now it’s a fantastic way to organize and listen to your music and videos, and is without peer in that regard. The iPod’s strengths are its web browsing and apps.
Hallo Raffael,
Danke für dieses sehr gute Beispiel – ich konnte es erfolgreich testen. Meine Frage nun, kann man einen Schreibvorgang einer fremden Anwendung in eine Datei (Logfile mit bestimmten Pfad und Namen) auch abfangen? Mich würden die Daten des jeweils letzten Anfügen in eine Logdatei interessieren.
Viele Grüße aus Erlangen
Dafür würde ich die FileSystemWatcher-Klasse nehmen. Damit kann man Dateien überwachen lassen. (Es wird dann z.B. ein EventHandler gefeuert, wenn sich eine Datei ändert.) Mehr dazu kannst du hier nachlesen: https://www.dotnetperls.com/filesystemwatcher
Hallo Raffael,
Danke für Deine schnelle Antwort!!! Der FileSystemWatcher in .Net FW ist bekannt, auch die Methoden und deren Events.
Meine Frage nun: Gibt es eine Möglichkeit unter Windows, das Schreibt-Event einer fremden (Task/Prozess bekannt) Applikation abzufangen, bevor diese (oder zeitgleich) in das Logfile schreibt?
Puh, da bin ich ehrlich gesagt überfragt. Wie gesagt, ich würde mich “von außen” mit dem FileSystemwatcher einklinken. Ob man schon vorher eingreifen kann, weiß ich leider nicht.
Hallo Raffael,
vielen Dank für Deine schnelle Antwort. Ja – mit diesem Thema hab ich schon einige Stunden verbracht und auch noch nicht die passende Lösung gefunden. Aber dennoch vielen Dank, eine gute Zeit und Grüße aus Erlagen.
Hey,
zuerst mal danke für dein tolles Beispiel.
Hast du vielleicht ein Tipp, wie man auf den neuen Menüeintrag reagieren kann wenn er sich nicht in der Menüleiste sondern im SubMenü zB. “Datei” befindet.
Danke
Das geht ebenfalls mit der oben genannten “InsertMenu”-Funktion. Du musst sie nur etwas anders aufrufen. Ein Beispiel hierzu findest du z.B. an dieser Stelle: http://pietschsoft.com/post/2008/03/Add-System-Menu-Items-to-a-Form-using-Windows-API
hallo Raffi,
danke für den tollen Artikel. Habe ihn aufmerksam durchgelesen. Sehr spannend.
Ich schreibe gerade an einem Thema das mit C Hacks zu tun hat. Da hole ich mir ab und an im Netz Unterstützung ;)
Viele Grüsse aus München