Control Extender

Problem

Wenn ich an meinem Computer Arbeite, habe ich den Fall, das ich 2 Monitore habe und mein Laptop, der neben den beiden Monitoren steht. Nun liegt es in der Natur der Sache, das ich auch alle 3 Monitore nutzen möchte, nur habe ich keine Lust immer Maus und Tastatur los zu lassen und mich zu meinem laptop rüber beugen, nur um ein anderes Fenster auszuwählen.
Daher wollte ich eine Möglichkeit, nur die Maus und Tastatur „fernzusteuern“.

Entwurf

Nun ist es nicht schwierig, Maus oder Tastatur per Software zu steuern und noch einfacher ist es, ein Programm Daten über das Netzwerk zu senden. Auch ist es recht einfach, die Aktuelle Position der Maus zu bestimmen.
Daher galt meine Aufmerksamkeit erst einmal der Frage, wie soll so ein System aussehen, das es praktisch nutzbar ist und welche Funktionen sollte es haben.
Dazu habe ich mir erst einmal eine „coole Features“ liste gemacht, also alles was eine solche Software leisten könnte:

  • Support für mehrere Monitore und Computer
  • „Platzierung“ der Monitore, sodass man einen Über dem aktuellen Monitor liegenden Monitor auch steuern kann.
  • Fenster Rüber schieben, sodass man z.B das Browserfenster von einem Computer und seinem Monitor auf den Monitor eines anderen Computers ziehen kann.
  • Support für Windows, Linux und Mac
  • Computer übergreifendes Drag&Drop und Copy&Paste
  • Programme auf einem 2 Computer sollten Per Alt+TAB in der Softwareliste erscheinen


Das sind ja nun schon eine menge Features, die nicht alle ganz so einfach zu implementieren sind.
Aber noch fehlt uns etwas, nämlich das Konzept zur Nutzung.
Die Frage dich sich uns hier stellt ist einfach: Wie soll das Programm bedient und genutzt werden?
Hier spielen natürlich Aspekte der Usability(tolles Buzzword), Systemintegration und Intuition eine Rolle.
Hier kommt es auf Kreativität an, sodass ich nur meine Lösung vorstellen Möchte:

Ich möchte nun einen Monitor steuern, der z.B Rechts von meinem Aktuellen steht, Intuitiv würde ich nun versuchen die Maus einfach nach rechts zu ziehen. Damit würde man nun aber an den rechten Rand des ersten Monitors stoßen und das wars dann. Nun war meine Überlegung, einen 1px Breiten „Fangstreifen“ eben dort einzurichten, sodass die Maus, sobald sie ihn auf ihm liegt, vom Programm „gefangen“ wird und nun alle Bewegungen auf den 2 Monitor übertragen werden, sodass man dort die Maus bewegen kann. Da nun der Fokus eh auf dem zweiten Monitor liegt, soll nun auch jegliche Maus-aktion wie tasten-klicken order Scrollen, aber auch Tastatur eingaben übertragen werden.
Will man nun aber den 2 Monitor über dem ersten Platzieren, so könnte man den „Fangstreifen“ eben auch an den oberen Bildschirmrand legen usw.

Implementierung

Da dieses Projekt erst einmal auf Eis gelegt wurde, Existiert nicht einmal eine Alpha-Version, sondern nur ein Proof-of-Concept Programm in C#.
Warum gerade C#, fragt sich der Aufmerksame Leser unseres C-Tutorials. Nun, das lag einfach nur daran, das C# eine sehr stark auf Windows angepasste Programmiersprache ist, die es dementsprechend einfacher macht auf Windows-Funktionen zurückzugreifen, als es in C oder C++ möglich wäre.
Nun ohne viel Erklärung der Sourcecode des Servers, der die Maus steuert:
Program.cs:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net.Sockets;
using System.IO;
using System.Threading;
 
namespace ControlExtender
{
    class Program
    {
        static NetworkAdapter na;
        static System.Net.IPAddress ip;
        static Thread th;
        static bool run = false;
        static void Main(string[] args)
        {
            ip = new System.Net.IPAddress(new byte[] { 192, 168, 42, 129 });
            TcpListener listener = new TcpListener(ip, 5000);
            HotKey hk = new HotKey();
            System.Diagnostics.ConsoleTraceListener cTL = new System.Diagnostics.ConsoleTraceListener();
            Console.WriteLine("Start");
            hk.OwnerForm = new System.Windows.Forms.Form();
            hk.AddHotKey(System.Windows.Forms.Keys.Q, HotKey.MODKEY.MOD_ALT | HotKey.MODKEY.MOD_CONTROL, "CE-HK");
            hk.HotKeyPressed += new HotKey.HotKeyPressedEventHandler(hk_HotKeyPressed);
            na = new NetworkAdapter(listener);
            th = new Thread(GetEnter);
            run = true;
            th.Start();
        }
 
        static void GetEnter()
        {
            while (run)
            {
                if (Console.ReadKey().Key == ConsoleKey.Enter)
                {
                    Console.WriteLine("Click");
                    na.SendClickCommand();
                }
            }
        }
 
        static void hk_HotKeyPressed(string HotKeyID)
        {
            Console.WriteLine("Hotkey!");
            na.SendClickCommand();
        }
    }
}


NetworkAdapter.cs:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net.Sockets;
using System.IO;
using System.Threading;
 
namespace ControlExtender
{
    class NetworkAdapter
    {
        Thread th;
        static StreamWriter sw;
        TcpListener listener;
        TcpClient c;
        Stream inOut;
        bool run;
 
        public NetworkAdapter(TcpListener listen)
        {
            listener = listen;
            run = true;
            listener.Start();
            c = listener.AcceptTcpClient();
            inOut = c.GetStream();
            sw = new StreamWriter(inOut);
            th = new Thread(SendMouseData);
            th.Start();
        }
 
        void SendMouseData()
        {
            try
            {
                while (run)
                {
                    Console.WriteLine("X:" + System.Windows.Forms.Cursor.Position.X.ToString() + " Y:" + System.Windows.Forms.Cursor.Position.Y.ToString());
                    sw.WriteLine("X:" + System.Windows.Forms.Cursor.Position.X.ToString() + " Y:" + System.Windows.Forms.Cursor.Position.Y.ToString());
                    sw.Flush();
                    Thread.Sleep(1);
                }
                c.Close();
                listener.Stop();
            }
            catch (Exception ex)
            {
                Console.WriteLine("Fehler!");
                Console.WriteLine(ex.Message);
                Console.WriteLine(ex.StackTrace);
                Thread.Sleep(10000);
                th.Abort();
            }
        }
 
        public void SendClickCommand()
        {
            try
            {
                sw.WriteLine("Click");
            }
            catch (Exception ex)
            {
                Console.WriteLine("Fehler!");
                Console.WriteLine(ex.Message);
                Console.WriteLine(ex.StackTrace);
                Thread.Sleep(10000);
                th.Abort();
            }
        }
    }
}


HotKey.cs by Tim Hartwig(Quelle):

using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Forms;
using System.Runtime.InteropServices;
 
namespace ControlExtender
{
 
    /// <summary>
    /// Mit dieser Klasse kann man sehr leicht eine globale Hotkey funktionalität in seinem Programm einbinden.
    /// Man muss nur dieser Klasse nur eine Form zuweisen die gesubclassed werden soll.
    /// Dann muss man nur noch ein paar eigene HotKey-Kombinationen registrieren (z.B. Strg+Alt+X) und diese
    /// mit dem Event abfragen bzw. abfangen. Dazu muss man eine eigene HotKeyID angeben um einen bestimmte HotKey
    /// Kombination später zu identifizieren wenn diese gedrückt wird. Wenn man z.B. eine Kombination registriert
    /// und ihr z.B. die HotKeyID "TEST1" zugewiesen wird dann kann man später im Event nach dieser ID "TEST1" fragen
    /// und dann eine Funktion aufrufen die für diesen HotKey bestimmt wurde.
    /// </summary>
    /// <remarks>Copyright © 2006 Tim Hartwig</remarks>
    public class HotKey : IMessageFilter
    {
        [DllImport("user32", CharSet = CharSet.Ansi, SetLastError = true, ExactSpelling = true)]
        private static extern int RegisterHotKey(IntPtr Hwnd, int ID, int Modifiers, int Key);
 
        [DllImport("user32", CharSet = CharSet.Ansi, SetLastError = true, ExactSpelling = true)]
        private static extern int UnregisterHotKey(IntPtr Hwnd, int ID);
 
        [DllImport("kernel32", EntryPoint = "GlobalAddAtomA", CharSet = CharSet.Ansi, SetLastError = true, ExactSpelling = true)]
        private static extern short GlobalAddAtom(string IDString);
 
        [DllImport("kernel32", CharSet = CharSet.Ansi, SetLastError = true, ExactSpelling = true)]
        private static extern short GlobalDeleteAtom(short Atom);
 
 
        public class HotKeyObject
        {
            private Keys mHotKey;
            private MODKEY mModifier;
            private string mHotKeyID;
            private short mAtomID;
 
            public Keys HotKey
            {
                get { return mHotKey; }
                set { mHotKey = value; }
            }
 
            public MODKEY Modifier
            {
                get { return mModifier; }
                set { mModifier = value; }
            }
 
            public string HotKeyID
            {
                get { return mHotKeyID; }
                set { mHotKeyID = value; }
            }
 
            public short AtomID
            {
                get { return mAtomID; }
                set { mAtomID = value; }
            }
 
            public HotKeyObject(Keys NewHotKey, MODKEY NewModifier, string NewHotKeyID)
            {
                mHotKey = NewHotKey;
                mModifier = NewModifier;
                mHotKeyID = NewHotKeyID;
            }
        }
 
        public Form OwnerForm
        {
            get { return mForm; }
            set { mForm = value; }
        }
 
        private Form mForm;
        private const int WM_HOTKEY = 786;
        private System.Collections.Generic.Dictionary<short, HotKeyObject> mHotKeyList = new System.Collections.Generic.Dictionary<short, HotKeyObject>();
        private System.Collections.Generic.Dictionary<string, short> mHotKeyIDList = new System.Collections.Generic.Dictionary<string, short>();
 
        /// <summary>
        /// Diesem Event wird immer die zugewiesene HotKeyID übergeben wenn eine HotKey Kombination gedrückt wurde.
        /// </summary>
        public event HotKeyPressedEventHandler HotKeyPressed;
        public delegate void HotKeyPressedEventHandler(string HotKeyID);
 
        public enum MODKEY : int
        {
            MOD_ALT = 1,
            MOD_CONTROL = 2,
            MOD_SHIFT = 4,
            MOD_WIN = 8
        }
 
        public HotKey()
        {
            Application.AddMessageFilter(this);
        }
 
        /// <summary>
        /// Diese Funktion fügt einen Hotkey hinzu und registriert ihn auch sofort
        /// </summary>
        /// <param name="KeyCode">Den KeyCode für die Taste</param>
        /// <param name="Modifiers">Die Zusatztasten wie z.B. Strg oder Alt, diese können auch mit OR kombiniert werden</param>
        /// <param name="HotKeyID">Die ID die der Hotkey bekommen soll um diesen zu identifizieren</param>
        public void AddHotKey(Keys KeyCode, MODKEY Modifiers, string HotKeyID)
        {
            if (mHotKeyIDList.ContainsKey(HotKeyID) == true) return; // TODO: might not be correct. Was : Exit Sub
 
            short ID = GlobalAddAtom(HotKeyID);
            mHotKeyIDList.Add(HotKeyID, ID);
            mHotKeyList.Add(ID, new HotKeyObject(KeyCode, Modifiers, HotKeyID));
            RegisterHotKey(mForm.Handle, (int)ID, (int)mHotKeyList[ID].Modifier, (int)mHotKeyList[ID].HotKey);
        }
 
        /// <summary>
        /// Diese Funktion entfernt einen Hotkey und deregistriert ihn auch sofort
        /// </summary>
        /// <param name="HotKeyID">Gibt die HotkeyID an welche entfernt werden soll</param>
        public void RemoveHotKey(string HotKeyID)
        {
            if (mHotKeyIDList.ContainsKey(HotKeyID) == false) return; // TODO: might not be correct. Was : Exit Sub
 
            short ID = mHotKeyIDList[HotKeyID];
            mHotKeyIDList.Remove(HotKeyID);
            mHotKeyList.Remove(ID);
            UnregisterHotKey(mForm.Handle, (int)ID);
            GlobalDeleteAtom(ID);
        }
 
        /// <summary>
        /// Diese Routine entfernt und Deregistriert alle Hotkeys
        /// </summary>
        public void RemoveAllHotKeys()
        {
            List<string> IDList = new List<string>();
            foreach (KeyValuePair<string, short> KVP in mHotKeyIDList)
            {
                IDList.Add(KVP.Key);
            }
 
            for (int i = 0; i <= IDList.Count - 1; i++)
            {
                RemoveHotKey(IDList[i]);
            }
        }
 
        public bool PreFilterMessage(ref Message m)
        {
            if (m.Msg == WM_HOTKEY)
            {
                if (HotKeyPressed != null)
                {
                    HotKeyPressed(mHotKeyList[(short)m.WParam].HotKeyID);
                }
            }
            return false;
        }
    }
}



Und noch der Source Code des Clienten:
Program.cs:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net.Sockets;
using System.IO;
using System.Threading;
 
namespace ControlExtenderClient
{
    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                TcpClient c = new TcpClient("Jochen-PC", 5000);
                Stream inOut = c.GetStream();
                StreamReader sr = new StreamReader(inOut);
                while (true)
                {
                    string s = sr.ReadLine();
                    Console.WriteLine(s);
                    if (s.Length < 3)
                    {
                        continue;
                    }
                    if (s.Contains("Click"))
                    {
                        MouseControl.Click();
                        Console.WriteLine("Click");
                        continue;
                    }
                    int x = 0, y = 0;
                    string XCoords, YCoords;
                    XCoords = s.Substring(0, s.IndexOf(" "));
                    YCoords = s.Substring(s.IndexOf(" "), s.Length - s.IndexOf(" "));
                    XCoords = XCoords.Substring(XCoords.IndexOf(":") + 1, XCoords.Length - (XCoords.IndexOf(":") + 1));
                    YCoords = YCoords.Substring(YCoords.IndexOf(":") + 1, YCoords.Length - (YCoords.IndexOf(":") + 1));
                    Console.WriteLine(XCoords);
                    Console.WriteLine(YCoords);
                    x = Convert.ToInt32(XCoords);
                    y = Convert.ToInt32(YCoords);
                    System.Windows.Forms.Cursor.Position = new System.Drawing.Point(x, y);
//                    MouseControl.Move(x, y);
                }
                c.Close();
            }
            catch (Exception ex)
            {
                Console.WriteLine("Fehler!");
                Console.WriteLine(ex.Message);
                Console.WriteLine(ex.StackTrace);
                Thread.Sleep(10000);
            }
        }
    }
}


MouseControl.cs:

using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.InteropServices;
using System.Windows.Forms;
 
 
namespace ControlExtenderClient
{
    public class MouseControl
    {
        [DllImport("user32.dll", EntryPoint = "SendInput", SetLastError = true)]
        static extern uint SendInput(uint nInputs, INPUT[] pInputs, int cbSize);
 
        [DllImport("user32.dll", EntryPoint = "GetMessageExtraInfo", SetLastError = true)]
        static extern IntPtr GetMessageExtraInfo();
 
        private enum InputType
        {
            INPUT_MOUSE = 0,
            INPUT_KEYBOARD = 1,
            INPUT_HARDWARE = 2,
        }
 
        [Flags()]
        private enum MOUSEEVENTF
        {
            MOVE = 0x0001,  // mouse move 
            LEFTDOWN = 0x0002,  // left button down
            LEFTUP = 0x0004,  // left button up
            RIGHTDOWN = 0x0008,  // right button down
            RIGHTUP = 0x0010,  // right button up
            MIDDLEDOWN = 0x0020,  // middle button down
            MIDDLEUP = 0x0040,  // middle button up
            XDOWN = 0x0080,  // x button down 
            XUP = 0x0100,  // x button down
            WHEEL = 0x0800,  // wheel button rolled
            VIRTUALDESK = 0x4000,  // map to entire virtual desktop
            ABSOLUTE = 0x8000,  // absolute move
        }
 
        [StructLayout(LayoutKind.Sequential)]
        private struct MOUSEINPUT
        {
            public int dx;
            public int dy;
            public int mouseData;
            public MOUSEEVENTF dwFlags;
            public int time;
            public IntPtr dwExtraInfo;
        }
 
        [StructLayout(LayoutKind.Sequential)]
        private struct INPUT
        {
            public InputType type;
            public MOUSEINPUT mi;
        }
        /// <summary>
        /// Diese Funktion bewegt den Mauscursor an einen bestimmten Punkt.
        /// </summary>
        /// <param name="x">X Koordinate der Position als absoluter Pixelwert</param>
        /// <param name="y">Y Koordinate der Position als absoluter Pixelwert</param>
        /// <returns>Liefert 1 bei Erfolg und 0, wenn der Eingabestream schon blockiert war zurück.</returns>
        public static uint Move(int x, int y)
        {
            // Bildschirm Auflösung
            float ScreenWidth = Screen.PrimaryScreen.Bounds.Width;
            float ScreenHeight = Screen.PrimaryScreen.Bounds.Height;
 
            INPUT input_move = new INPUT();
            input_move.type = InputType.INPUT_MOUSE;
 
            input_move.mi.dx = (int)Math.Round(x * (65535 / ScreenWidth), 0);
            input_move.mi.dy = (int)Math.Round(y * (65535 / ScreenHeight), 0);
            input_move.mi.mouseData = 0;
            input_move.mi.dwFlags = (MOUSEEVENTF.MOVE | MOUSEEVENTF.ABSOLUTE);
            input_move.mi.time = 0;
            input_move.mi.dwExtraInfo = GetMessageExtraInfo();
 
            INPUT[] input = { input_move };
            return SendInput(1, input, Marshal.SizeOf(input_move));
        }
 
        /// <summary>
        /// Diese Funktion simuliert einen einfach Mausklick mit der linken Maustaste an der aktuellen Cursurposition.
        /// </summary>
        /// <returns>Liefert 2 zurück, wenn beide Aktionen (Maus down und Maus up) erfolgreich waren. Andernfalls 1 oder 0.</returns>
        public static uint Click()
        {
            INPUT input_down = new INPUT();
            input_down.type = InputType.INPUT_MOUSE;
 
            input_down.mi.dx = 0;
            input_down.mi.dy = 0;
            input_down.mi.mouseData = 0;
            input_down.mi.dwFlags = MOUSEEVENTF.LEFTDOWN;
            input_down.mi.time = 0;
            input_down.mi.dwExtraInfo = GetMessageExtraInfo();
 
            INPUT input_up = input_down;
            input_up.mi.dwFlags = MOUSEEVENTF.LEFTUP;
 
            INPUT[] input = { input_down, input_up };
            return SendInput(2, input, Marshal.SizeOf(input_down));
        }
    }
}