Jacek Matulewski 21 listopada 2016 Programowanie Windows WinAPI i platforma .NET Jacek Matulewski 21 listopada 2016 http://www.fizyka.umk.pl/~jacek/dydaktyka/winprog/
Mechanizm Platform Invoke w platformie .NET Programowanie Windows Mechanizm Platform Invoke w platformie .NET
Trivia Platformy .NET, WinRT (W8), UWP (W10) tworzą osobną warstwę z własną „wirtualną maszyną” Platforma .NET ułatwia odwołania do niezarządzanych DLL via mechanizm PInvoke Ważne źródło: http://www.pinvoke.net/index.aspx (deklaracja, przykłady użycia, zamienniki w .NET) Tutorial w MSDN: https://msdn.microsoft.com/en-us/library/aa288468(v=vs.71).aspx
Trivia Czynności wykonywane przy Platform Invoke: lokalizacja biblioteki eksportującej funkcję załadowanie jej do pamięci (przy pierwszym uruchomieniu) pobranie adresu funkcji wrzucanie argumentów na stos i wywołanie funkcji (przekazanie kontroli wątku do kodu niezarządzanego) pobranie i konwersja typu zwracanej wartości powrót kontroli do kodu zarządzanego
Trivia https://msdn.microsoft.com/en-us/library/aa719485(v=vs.71).aspx
Atrybut DllImport Atrybut DllImportAttribute pozwala na zadeklarowanie metody statycznej, która reprezentuje funkcję z biblioteki DLL Konstruktor przyjmuje nazwę niezarządzanej biblioteki DLL: public DllImportAttribute(string dllName) Wymaga zadeklarowania przestrzeni nazw: using System.Runtime.InteropServices; Składnia (w najprostszej formie): [DllImport("Biblioteka.dll")] static extern Funkcja(argumenty); Problem konwersji typów
Atrybut DllImport Oryginalna sygnatura funkcji WinAPI: BOOL WINAPI MessageBeep( _In_ UINT uType //sound type ); Import (może być w klasie statycznej, ale niekoniecznie): [DllImport("User32.dll")] static extern bool MessageBeep(uint uType); Odpowiednik w platformie .NET: System.Media.SystemSounds.Beep.Play
Atrybut DllImport Przykład użycia: object sender, EventArgs e) { private void button1_Click( object sender, EventArgs e) { MessageBeep(0); MessageBeep(48); }
Atrybut DllImport Oryginalna sygnatura funkcji WinAPI: int WINAPI MessageBox( _In_opt_ HWND hWnd, _In_opt_ LPCTSTR lpText, _In_opt_ LPCTSTR lpCaption, _In_ UINT uType ); Przykładowe wartości uType: MB_OK (0), MB_YESNO (4), MB_ABORTRETRYIGNORE (2), MB_HELP (16384), MB_CANCELTRYCONTINUE (6), MB_ICONERROR (16), MB_ICONQUESTION (32), ...
Atrybut DllImport Import: Zmiana nazwy funkcji – własność EntryPoint [DllImport("user32.dll", EntryPoint = "MessageBox", CharSet = CharSet.Unicode)] static extern int _MessageBox(IntPtr hWnd, String text, String caption, uint type); Zmiana nazwy funkcji – własność EntryPoint Zestaw znaków w argumentach-łańcuchach – CharSet (w WinAPI – tablica znaków, wersja A i W) Odpowiednik w platformie .NET: System.Windows.Forms.MessageBox.Show
Atrybut DllImport Przykład użycia: Odpowiednik w platformie .NET: private void button1_Click(object sender, EventArgs e) { _MessageBox(this.Handle, "Polskie litery: ąćęłńóśżź", "Tytuł", 64); } Odpowiednik w platformie .NET: MessageBox.Show( "Polskie litery: ąćęłńóśżź", "Tytuł", MessageBoxButtons.OK, MessageBoxIcon.Information);
Użycie typów wyliczeniowych Oryginalna sygnatura funkcji WinAPI: UINT WINAPI WinExec( _In_ LPCSTR lpCmdLine, _In_ UINT uCmdShow ); Wybrane wartości uCmdShow: SW_HIDE (0), SW_MAXIMIZE (3), SW_MINIMIZE (6), SW_RESTORE (9), SW_SHOW (5), SW_DEFAULT (10) Te same wartości w ShowWindow, CreateProcess itd.
Typy wyliczeniowe Import: [DllImport("kernel32.dll")] static extern uint WinExec(string polecenie, uint stanOkna); To nie jest zgodne z duchem platformy .NET – typ wyliczeniowy Te sam problem w MessageBeep i MessageBox Odpowiednik w platformie .NET: System.Diagnostics.Process.Start
Typy wyliczeniowe Typ wyliczeniowy z określonym typem uint - konwersja: public enum StanOkna : uint { Ukryte = 0, Normalme, Zminimalizowane, Zmaksymalizowane, Nieaktywne, Domyślne = 10 }; Import: [DllImport("kernel32.dll")] static extern uint WinExec(string polecenie, StanOkna stanOkna);
Atrybut DllImport Przykład użycia: EventArgs e) { private void button1_Click(object sender, EventArgs e) { uint wynik = WinExec(textBox1.Text, StanOkna.Normalme); if (wynik <= 31) MessageBox.Show("Błąd " + wynik + "! Nie udało się uruchomić " + textBox1.Text); }
Atrybut DllImport Przykład użycia z listą wyboru (ComboBox): private void button1_Click(object sender, EventArgs e) { uint wynik = WinExec(textBox1.Text, (StanOkna)cbStanOkna.SelectedIndex); if (wynik <= 31) MessageBox.Show("Błąd " + wynik + "! Nie udało się uruchomić " + textBox1.Text); }
Demo Funkcje MessageBeep, MessageBox, WinExec Import, wskazanie nazwy funkcji, typ wyliczeniowy
Zwracanie wartości przez referencje Oryginalna sygnatura funkcji WinAPI: BOOL GetDiskFreeSpaceEx( LPCTSTR lpDirectoryName, PULARGE_INTEGER lpFreeBytesAvailableToCaller, PULARGE_INTEGER lpTotalNumberOfBytes, PULARGE_INTEGER lpTotalNumberOfFreeBytes ); Typy użyte w sygnaturze (zob. MSDN Windows Data Types): LPCTSTR = LPCWSTR (Unicode) lub LPCSTR (ASCII) typedef __nullterminated CONST CHAR *LPCSTR; PULARGE_INTEGER = wskaźnik do LARGE_INTEGER
Zwracanie wartości przez referencje Liczba całkowita 64-bitowa: typedef union _LARGE_INTEGER { struct { DWORD LowPart; LONG HighPart; }; struct { DWORD LowPart; LONG HighPart; } u; LONGLONG QuadPart; } LARGE_INTEGER, *PLARGE_INTEGER; Liczba całkowita 64-bitowa: W przypadku, gdy kompilator obsługuje liczby 64-bitowe, należy używać typu LONGLONG. Jeżeli nie – dwóch DWORD.
Zwracanie wartości przez referencje Import: [DllImport("kernel32.dll")] static extern bool GetDiskFreeSpaceEx( string katalog, ref long wolneMiejsceDlaUzytkownika, ref long rozmiarDysku, ref long wolneMiejsceNaDysku); Odpowiednik w platformie .NET: System.IO.DriveInfo
Zwracanie wartości przez referencje Przykład użycia: private int wolneMiejsceNaDysku( string katalogGlownyDysku) { int wolneMiejsceNaDyskuProcenty; long wolneMiejsceDlaUzytkownika = 0; long rozmiarDysku = 0; long wolneMiejsceNaDysku = 0; if (GetDiskFreeSpaceEx( katalogGlownyDysku, ref wolneMiejsceDlaUzytkownika, ref rozmiarDysku, ref wolneMiejsceNaDysku)) { ...
Zwracanie wartości przez referencje Przykład użycia: private int wolneMiejsceNaDysku( string katalogGlownyDysku) { ... if (GetDiskFreeSpaceEx( ... ) { wolneMiejsceNaDyskuProcenty = (int)(100 * (rozmiarDysku – wolneMiejsceNaDysku) / (double)rozmiarDysku); } else wolneMiejsceNaDyskuProcenty = -1; return wolneMiejsceNaDyskuProcenty; }
Zwracanie wartości przez referencje Przykład użycia: public Form1() { InitializeComponent(); string katalogGłównyDysku = System.Environment.GetLogicalDrives()[0]; int procentZajetosci = wolneMiejsceNaDysku(katalogGlownyDysku); if (procentZajetosci >= 0) progressBar1.Value = procentZajetosci; }
Demo Funkcja GetDiskFreeSpaceEx Zwracanie wartości przez referencje
Tablica znaków – łańuchy Oryginalna sygnatura funkcji WinAPI: UINT WINAPI GetWindowsDirectory( _Out_ LPTSTR lpBuffer, // _In_ UINT uSize ); ASCII: typedef CHAR *LPSTR; UNICODE: typedef WCHAR *LPWSTR; Import: [DllImport("kernel32.dll")] static extern uint GetWindowsDirectory( StringBuilder bufor, uint rozmiarBufora); Odpowiednik w platformie .NET: System.Environment.SystemDirectory
Tablica znaków – łańuchy Przykład użycia (StringBuilder wymaga trochę wysiłku): private void button4_Click(object sender, EventArgs e) { const int MAX_PATH = 260; StringBuilder katalogWindows = new StringBuilder(MAX_PATH); uint rozmiar = (uint)katalogWindows.Capacity; GetWindowsDirectory(katalogWindows, rozmiar); MessageBox.Show(katalogWindows.ToString()); } Tak samo funkcje: GetWindowsDirectory i GetSystemDirectory
Color Picker Cel: chcemy pobierać kolor piksela na ekranie znajdujący się pod kursorem myszy Wyłączamy skalowanie aplikacji – skalowanie pozycji myszy: BOOL WINAPI SetProcessDPIAware(void); Pobieramy pozycję myszy: BOOL WINAPI GetPhysicalCursorPos( _Out_ LPPOINT lpPoint ); Pobieramy obraz (bitmapę) ze wskazanego prostokąta (np. 1×1): BOOL BitBlt(_In_ HDC hdcDest, _In_ int nXDest, _In_ int nYDest, _In_ int nWidth, _In_ int nHeight, _In_ HDC hdcSrc, _In_ int nXSrc, _In_ int nYSrc, _In_ DWORD dwRop); Odpowiednik w platformie .NET: System.Windows.Forms.Cursor.Position Odpowiednik w platformie .NET: System.Drawing.Graphics.CopyFromScreen
Color Picker Import: public static extern bool SetProcessDPIAware(); [DllImport("user32.dll")] public static extern bool SetProcessDPIAware(); public static extern bool GetCursorPos(ref Point lpPoint); public static extern bool GetPhysicalCursorPos(ref Point lpPoint); [DllImport("gdi32.dll", CharSet = CharSet.Auto, SetLastError = true, ExactSpelling = true)] public static extern int BitBlt(IntPtr hDC, int x, int y, int nWidth, int nHeight, IntPtr hSrcDC, int xSrc, int ySrc, int dwRop);
Color Picker Przykład użycia (z projektu Huberta Wojtowicza): private static Bitmap pikselEkranu = new Bitmap( 1, 1, System.Drawing.Imaging.PixelFormat.Format32bppArgb); public static Color PobierzKolorPiksela(Point położenie) { //grafika źródła (ekran) using (Graphics gd = Graphics.FromImage(pikselEkranu)) //grafika bitmapy, w której zostanie zapisany kolor using (Graphics gs = Graphics.FromHwnd(IntPtr.Zero)) { ... } return screenPixel.GetPixel(0, 0); //zwracany jedyny piksel
Color Picker Przykład użycia (z projektu Huberta Wojtowicza): private static Bitmap pikselEkranu = new Bitmap( 1, 1, System.Drawing.Imaging.PixelFormat.Format32bppArgb); public static Color PobierzKolorPiksela(Point położenie) { ... IntPtr hsDC = gs.GetHdc(); //uchwyt do DC źródła IntPtr hdDC = gd.GetHdc(); //uchwyt do DC celu BitBlt( hdDC, 0, 0, 1, 1, //cel (uchwyt i geometria) hsDC, położenie.X, położenie.Y, //źródło (uchwyt i poł.) (int)CopyPixelOperation.SourceCopy); //tryb wklejania gd.ReleaseHdc(); gs.ReleaseHdc(); //zwalnianie uchwytów }
Color Picker Przykład użycia (wymaga kontrolek Timer, Label, Panel): private void timer1_Tick(object sender, EventArgs e) { Point pozycjaKursora = Cursor.Position; GetPhysicalCursorPos(ref pozycjaKursora); label3.Text = pozycjaKursora.ToString(); panel1.BackColor = PobierzKolorPiksela(pozycjaKursora); }
Zrzut całego ekranu Przykład użycia (zmodyfikowana funkcja pobierająca piksel): public static Bitmap PobierzZrzutEkranu() { RECT obszar = new RECT(); GetWindowRect(GetDesktopWindow(), out obszar); Bitmap bitmap = new Bitmap(obszar.Right - obszar.Left, obszar.Bottom - obszar.Top); using (Graphics gd = Graphics.FromImage(bitmap)) using (Graphics gs = Graphics.FromHwnd(IntPtr.Zero)) ... } return bitmap;
Zrzut całego ekranu Przykład użycia (zmodyfikowana funkcja pobierająca piksel): public static Bitmap PobierzZrzutEkranu() { ... IntPtr hsDC = gs.GetHdc(); IntPtr hdDC = gd.GetHdc(); BitBlt( hdDC, 0, 0, obszar.Right - obszar.Left, obszar.Bottom - obszar.Top, hsDC, 0, 0, (int)CopyPixelOperation.SourceCopy); gd.ReleaseHdc(); gs.ReleaseHdc(); }
Zrzut całego ekranu Deklarowanie struktur danych na potrzeby PInvoke: [StructLayout(LayoutKind.Sequential)] public struct RECT { public int Left; public int Top; public int Right; public int Bottom; }
Zrzut ekranu w obrębie okna Zamiast funkcji BitBlt można użyć PrintWindow (służy do kopiowania zawartości okna do wskazanego DC) Nie pozwala na operacje bitowe przy kopiowaniu Oryginalna sygnatura funkcji WinAPI: BOOL PrintWindow(HWND hwnd, HDC hdcBlt, UINT nFlags); Import: [DllImport("user32.dll")] static extern bool PrintWindow( IntPtr hWnd, IntPtr hDC, int flags);
Zrzut ekranu w obrębie okna Przykład użycia: public static Bitmap PobierzZrzutOkna(IntPtr uchwytOkna, bool tylkoObszarKlienta = false) { RECT obszar = new RECT(); GetWindowRect(uchwytOkna, out obszar); Bitmap bitmap = new Bitmap(obszar.Right - obszar.Left, obszar.Bottom - obszar.Top); using (Graphics gd = Graphics.FromImage(bitmap)) PrintWindow(uchwytOkna, gd.GetHdc(), tylkoObszarKlienta?1:0); } return bitmap;
Demo Funkcja BitBlt, PrintWindow Pobieranie zrzutów ekanu, color picker
Demo Funkcja EnumWindows, EnumChildWindows Zadanie domowe: na zrzucie ekranu zaznaczyć położenia wszystkich okien (w tym kontrolek) z tytułem i uchwytem
Atrybut DllImport Niektóre pola i własności atrybutu DllImportAttribute: CallingConvention (Cdecl, StdCall, ThisCall, Winapi) CharSet (zestaw znaków: Ansi, Unicode) EntryPoint (nazwa importowanej funkcji) SetLastError (wywoływana jest metoda SetLastError, jej wynik można pobrać funkcją GetLastError) MarshalAsAttribute – określa niezarządzany typ argumentu lub wartości zwracanej przez funkcję; zwykle zbędny; jedyny wyjątek to niejednoznaczność łańcuchów [DllImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] static extern bool MessageBeep(uint uType); Atrybuty In i Out określają kierunek konwersji argumentów
Atrybut MarshalAs Przykład dla argumentów (zał. Unicode): [DllImport("User32.dll", CharSet = CharSet.Unicode)] [return: MarshalAs(UnmanagedType.I4)] static extern Int32 MessageBoxEx( IntPtr hWnd, //unicode [param: MarshalAs(UnmanagedType.LPTStr)] String lpText, [param: MarshalAs(UnmanagedType.LPTStr)] String lpCaption, //32-bitowa (4-bajtowa) liczba całkowita bez znaku [param: MarshalAs(UnmanagedType.U4)] UInt32 uType, //16-bitowa (2-bajtowa) liczba całkowita bez znaku [param: MarshalAs(UnmanagedType.U2)] UInt16 wLanguageId); https://www.codeproject.com/articles/66245/marshaling-with-csharp-chapter-1-introducing-marsh.aspx
Programowanie Windows Komunikaty Windows
Identyfikacja adresata komunikatu Funkcja FindWindow (już poznaliśmy) zwraca uchwyt okna o podanej klasie lub tytule HWND WINAPI FindWindow( _In_opt_ LPCTSTR lpClassName, _In_opt_ LPCTSTR lpWindowName ); Import: [DllImport("user32.dll")] static extern HWND FindWindow(string nazwaKlasy, string nazwaOkna);
Wysyłanie komunikatu Funkcje SendMessage i PostMessage (także już znane) wysyłają komunikaty do okna o podanym uchwycie LRESULT WINAPI SendMessage( _In_ HWND hWnd, _In_ UINT Msg, _In_ WPARAM wParam, _In_ LPARAM lParam); Import: [DllImport("user32.dll")] static extern int SendMessage( HWND hwnd, uint Msg, int wParam, int lParam);
Wysyłanie komunikatu Stałe: Lista komunikatów: public const int WM_CLOSE = 0x0010; public const int WM_SYSCOMMAND = 0x0112; public const int WM_NCMOUSEMOVE = 0x00A0; public const int WM_PAINT = 0x000F; public const int WM_KEYDOWN = 0x0100; public const int WM_KEYUP = 0x0101; public const int WM_CHAR = 0x0102; Lista komunikatów: http://www.pinvoke.net/default.aspx/Enums/WindowsMessages.html
Wysyłanie komunikatu Przykład użycia: { private void button1_Click(object sender, EventArgs e) { string nazwaOkna = textBox1.Text; HWND uchwyt = FindWindow(null, nazwaOkna); if (uchwyt != HWND.Zero) SendMessage(uchwyt, WM_CLOSE, 0, 0); else MessageBox.Show("Nie ma okna o tytule \"" + nazwaOkna + "\""); }
Odbieranie komunikatu Nadpisywanie procedury okna (ListBox, MultiColumn = true, ColumnWidth = 30): protected override void WndProc(ref Message m) { if (m.Msg!=308) listBox1.Items.Add(m.Msg); //308 = WM_CTLCOLORLISTBOX base.WndProc(ref m); }
Odbieranie komunikatu Nadpisywanie procedury okna (ListBox, MultiColumn = true, ColumnWidth = 30): switch(m.Msg) { case WM_NCMOUSEMOVE: long lParam=(long)m.LParam; long x=lParam & 0x0000FFFF; long y=(lParam & 0xFFFF0000) >> 16; label1.Text = "(Komunikat) Mysz poza obszarem klienta: " + x + "," + y; break; }