Из книги C#. Советы программистам (в сокращении)
Программный код, который выполняется под управлением CLR (Common Language Runtime, т. е. общая среда выполнения языков), называется управляемым (managed) кодом. Программный код, выполняющийся вне среды выполнения CLR, называется неуправляемым (unmanaged) кодом. Примером неуправляемого программного кода служат функции Win32 API, компоненты COM, интерфейсы ActiveX. Несмотря на большое количество классов .NET Framework, содержащих множество методов, программисту все равно приходится иногда прибегать к неуправляемому коду. Надо сказать, что число вызовов неуправляемого кода уменьшается с выходом каждой новой версии .NET Framework. Microsoft надеется, что наступит такое время, когда весь код можно будет сделать управляемым и безопасным. Но пока реальность такова, что без вызовов функций Windows API нам пока не обойтись. Но сначала немного теории.
Управляемый код .NET Framework может вызывать неуправляемую функцию из DLL (функцию Windows API) при помощи специального механизма Platform Invoke (сокр. P/Invoke). Для того чтобы обратиться к какой-нибудь неуправлямойнеуправляемой библиотеке DLL, вы должны преобразовать .NET-объекты в наборы struct, char* и указателей на функции, как того требует язык C. Как сказали бы программисты на своем жаргоне — вам нужно маршалировать параметры. Более подробно о маршалинге (Marshalling) вам следует почитать в документации. Чтобы вызвать DLL-функцию из C#, сначала ее необходимо объявить (программисты, имеющие опыт работы с Visual Basic 6.0, уже знакомы с этим способом). Для этого используется атрибут DllImport:
using System.Runtime.InteropServices;
public class Win32
{
[DllImport("User32.Dll")]
public static extern void SetWindowText(IntPtr hwnd,
String lpString);
}
Иногда в примерах вы можете также встретить такой способ (длинный и неудобный): [System.Runtime.InteropServices.DllImport("User32.Dll")]..., но это на любителя.
Атрибут DllImport сообщает компилятору, где находится точка входа, что позволяет далее вызывать функцию из нужного места. Вы должны всегда использовать тип IntPtr для HWND, HMENU и любых других описателей. Для LPCTSTR используйте String, а сервисы взаимодействия (interop services) выполнят автоматический маршаллинг System.String в LPCTSTR до передачи в Windows. Компилятор ищет указанную выше функцию SetWindowText в файле User32.dll и перед ее вызовом автоматически преобразует вашу строку в LPTSTR (TCHAR*). Почему это происходит? Для каждого типа в C# определен свой тип, используемый при маршалинге по умолчанию (default marshaling type) . Для строк это LPTSTR.
Предположим, нам необходимо вызвать функцию GetWindowText, у которой имеется строковый выходной параметр char*. По умолчанию, для строк используется LPTSTR, но если мы будем использовать System.String, как было сказано выше, то ничего не произойдет, так как класс System.String не позволяет модифицировать строку. Вам необходимо использовать класс StringBuilder, который позволяет изменять строки.
using System.Text; // для StringBuilder
[DllImport("user32.dll")]
public static extern int GetWindowText(IntPtr hwnd,
StringBuilder buf, int nMaxCount);
Тип, используемый для маршашлинга StringBuilder по умолчанию, — тоже LPTSTR, зато теперь GetWindowText может модифицировать саму вашу строку:
StringBuilder sTitleBar = new StringBuilder(255);
GetWindowText(this.Handle, sTitleBar, sTitleBar.Capacity);
MessageBox.Show(sTitleBar.ToString());
Таким образом, ответом на вопрос, как вызывать функцию, у которой есть выходной строковый параметр, будет — используйте класс StringBuilder.
Например, мы хотим вызвать функцию GetClassName, который принимает параметр LPSTR (char*) даже в Unicode-версиях. Если вы передадите строку, общеязыковая исполняющая среда (CLR) преобразует ее в серию TCHAR. Но с помощью атрибута MarshalAs можно переопределить то, что предлагается по умолчанию:
[DllImport("user32.dll")]
public static extern int GetClassName(IntPtr hwnd,
[MarshalAs(UnmanagedType.LPStr)] StringBuilder buf,
int nMaxCount);
Теперь, когда вы вызовете GetClassName, .NET передаст вашу строку в виде символов ANSI, а не "широких символов".
Возьмем для примера функцию GetWindowRect, которая записывает в структуру RECT экранные координаты окна. Чтобы вызвать функцию GetWindowRect и передать ей структуру RECT нужно использовать тип struct в сочетании с атрибутом StructLayout:
[StructLayout(LayoutKind.Sequential)]
public struct RECT
{
public int left ;
public int top;
public int right;
public int bottom;
}
[DllImport("user32.dll")]
public static extern int GetWindowRect(IntPtr hwnd, ref RECT rc);
Важно использовать ref, чтобы CLR передала параметр типа RECT как ссылку. В этом случае функция сможет модифицировать ваш объект, а не его безымянную копию в стеке. После такого объявления функции можно ее вызвать в коде:
int w, h;
RECT rc = new RECT();
GetWindowRect(this.Handle, ref rc);
w = rc.right - rc.left;
h = rc.bottom - rc.top;
MessageBox.Show("Ширина формы: " + w + "\n\rВысота формы: " + h);
Обратите внимание, что ref используется и в объявлении, и при вызове функции. Тип, по умолчанию применяемый для маршалинга типов struct — по умолчанию LPStruct, поэтому необходимости в атрибуте MarshalAs нет. Но если вы хотите использовать RECT в виде класса, а не struct, вам необходимо реализовать оболочку:
// Если RECT - класс, а не структура (struct)
[DllImport("user32.dll")]
public static extern int GetWindowRect(IntPtr hwnd,
[MarshalAs(UnmanagedType.LPStruct)] RECT rc);
Для использования функций, написанных на C#, в качестве функций обратного вызова Windows, нужно использовать делегаты (delegate).
delegate bool EnumWindowsCB(int hwnd, int lparam);
Объявив свой тип делегата, можно написать оболочку для функции Windows API:
[DllImport("user32")]
public static extern int EnumWindows(EnumWindowsCB cb, int lparam);
Так как в строке с delegate просто объявляется тип делегата (delegate type), сам делегат нужно предоставить в классе:
// В вашем классе
public static bool MyEWP(int hwnd, int lparam)
{
// Делаем тут чего-то
return true;
}
а затем передать оболочке:
EnumWindowsCB cb = new EnumWindowsCB(MyEWP);
Win32.EnumWindows(cb, 0);
Проницательные читатели заметят, что я умолчал о проблеме с lparam.
В языке C, если в EnumWindows дается LPARAM, Windows будет уведомлять вашу функцию обратного вызова этим LPARAM. Обычно lparam — указатель на некую структуру или класс, который содержит контекстную информацию, нужную вам для выполнения своих операций. Но запомните: в .NET слово «указатель» произносить нельзя! Так что же делать? Можно объявить ваш lparam как IntPtr и использовать GCHandle в качестве его оболочки:
// lparam — теперь IntPtr
delegate bool EnumWindowsCB(int hwnd, IntPtr lparam);
// Помещаем объект в оболочку GCHandle
MyClass obj = new MyClass();
GCHandle gch = GCHandle.Alloc(obj);
EnumWindowsCB cb = new EnumWindowsCB(MyEWP);
Win32.EnumWindows(cb, (IntPtr)gch);
gch.Free();
Не забудьте вызвать Free, когда закончите свои дела! Иногда в C# приходится самому освобождать память. Чтобы получить доступ к «указателю» lparam внутри перечислителя, используйте GCHandle.Target.
public static bool MyEWP(int hwnd, IntPtr param)
{
GCHandle gch = (GCHandle)param;
MyClass c = (MyClass)gch.Target;
// ...пользуемся
return true;
}
Ниже показан написанный мной класс, который инкапсулирует EnumWindows в массив. Вместо того чтобы возиться со всеми этими делегатами и обратными вызовами, вы пишете:
WindowArray wins = new WindowArray();
foreach (int hwnd in wins)
{
// Делаем тут чегото
}
// WinArray: генерирует ArrayList окон верхнего уровня,
// используя EnumWindows
//
using System;
using System.Collections;
using System.Runtime.InteropServices;
namespace WinArray
{
public class WindowArray : ArrayList {
private delegate bool EnumWindowsCB(int hwnd, IntPtr param);
// Объявляются как private, потому что нужны лишь мне
[DllImport("user32")]
private static extern int EnumWindows(EnumWindowsCB cb,
IntPtr param);
private static bool MyEnumWindowsCB(int hwnd, IntPtr param)
{
GCHandle gch = (GCHandle)param;
WindowArray itw = (WindowArray)gch.Target;
itw.Add(hwnd);
return true;
}
// Это единственный открытый (public) метод.
// Только его вам и надо вызывать.
public WindowArray()
{
GCHandle gch = GCHandle.Alloc(this);
EnumWindowsCB ewcb = new EnumWindowsCB(MyEnumWindowsCB);
EnumWindows(ewcb, (IntPtr)gch);
gch.Free();
}
}
}
Небольшая программа ListWin (Приложение ListWin.cs), которую я написал для перечисления окон верхнего уровня, позволяет просматривать списки HWND, имен классов, заголовков и/или прямоугольников окон, используя RECT или Rectangle. Исходный код ListWin показан не полностью; весь исходный код можно скачать по ссылке, приведенной в конце статьи.
Можно создать собственную управляемую библиотеку, из которой можно будет вызывать функции Windows API. Для этого в Visual Studio предусмотрены специальные опции. Новый проект создается как библиотека классов (Class Library). Сборка при этом автоматически получает расширение dll. Использовать управляемую библиотеку в управляемом коде просто. Для этого надо добавить ссылку (используя меню Project | Add Reference…) на библиотечную сборку, указав месторасположение сборки в соответствующем диалоговом окне. После этого Visual Studio копирует сборку в директорию, в которой располагается разрабатываемый код. Далее в коде программы используется либо оператор using, либо полное имя библиотечного модуля с точечной нотацией. Все библиотечные классы и методы готовы к использованию в коде программы.
Также можно воспользоваться командной строкой. Чтобы скомпилировать класс Win32API.cs, введите:
csc /t:library /out:Win32API.dll Win32API.cs
В результате у вас будет создан файл Win32API.dll, доступный для любого проекта на C#.
using Win32API // подключаем класс из библиотеки DLL
int hwnd = // ваш код
string s = "Моя строка";
Win32.SetWindowText(hwnd, s); // вызываем функцию из нашей библиотеки
Пример создания библиотеки и использования функций Windows API находится в папке Win32 на прилагаемом к книге диске.
Вкратце ознакомившись с теорией, перейдем к конкретным примерам. В предыдущих главах я уже неоднократно приводил пример использования функций Windows API для решения различных проблем. Рассмотрим еще несколько полезных советов, которые не вошли в другие главы.
Несмотря на огромное число имеющихся классов .NET Framework, программисту по-прежнему приходится прибегать к вызовам системных функций Windows API. В папке Win32Help на прилагаемом к книге компакт-диске вы найдете демо-версию справочника по функциям Windows API для .NET Framework. Если вам понравится этот справочник, то вы можете приобрести его полную версию на моем сайте.
// Win32API: оболочка для избранных функций Win32 API
// Для компиляции наберите команду:
// csc /t:library /out:Win32API.dll Win32API.cs
using System;
using System.Drawing;
using System.Text;
using System.Runtime.InteropServices;
// Пространство имен для ваших Win32функций.
// Добавляйте их сюда по мере необходимости...
//
namespace Win32API
{
[StructLayout(LayoutKind.Sequential)]
public struct POINT
{
public POINT(int xx, int yy) { x=xx; y=yy; }
public int x;
public int y;
public override string ToString()
{
String s = String.Format("({0},{1})", x, y);
return s;
}
}
[StructLayout(LayoutKind.Sequential)]
public struct SIZE
{
public SIZE(int cxx, int cyy) { cx=cxx; cy=cyy; }
public int cx;
public int cy;
public override string ToString()
{
String s = String.Format("({0},{1})", cx, cy);
return s;
}
}
[StructLayout(LayoutKind.Sequential)]
public struct RECT
{
public int left;
public int top;
public int right;
public int bottom;
public int Width() { return right left; }
public int Height() { return bottom top; }
public POINT TopLeft() { return new POINT(left, top); }
public SIZE Size() { return new SIZE(Width(), Height()); }
public override string ToString()
{
String s = String.Format("{0}x{1}", TopLeft(), Size());
return s;
}
}
public class Win32
{
[DllImport("user32.dll")]
public static extern bool IsWindowVisible(int hwnd);
[DllImport("user32.dll")]
public static extern int GetWindowText(int hwnd,
StringBuilder buf, int nMaxCount);
[DllImport("user32.dll")]
public static extern int GetClassName(int hwnd,
[MarshalAs(UnmanagedType.LPStr)] StringBuilder buf,
int nMaxCount);
[DllImport("user32.dll")]
public static extern int GetWindowRect(int hwnd, ref RECT rc);
[DllImport("user32.dll")]
// Заметьте, что исполняющая среда знает,
// как выполнить маршалинг Rectangle
public static extern int GetWindowRect(int hwnd, ref Rectangle rc);
// ...продолжайте добавлять нужные функции
}
}
using System;
using System.Text;
using System.Drawing;
using System.Diagnostics;
using System.Runtime.InteropServices;
using Win32API; // самодельная оболочка для Win32 API
using WinArray; // самодельный перечислитель окон
class MyApp
{
// Глобальные ключи командной строки
static bool bRectangle = false; // показывает прямоугольник окна,
// используя Rectangle
static bool bRect = false; // показывает прямоугольник окна,
// используя RECT
static bool bClassName = false; // показывает имя класса
static bool bTitle = false; // показывает заголовок
static bool bHwnd = false; // показывает HWND
[STAThread]
// Main — главная точка входа
static int Main(string[] args) {
// Разбираем командную строку.
// Ключи могут быть указаны в любом порядке.
if (args.GetLength(0)<=0)
return help();
for (int i=0, len=args.GetLength(0); i<len; i++)
{
if (args[i].StartsWith("/") || args[i].StartsWith("") )
{
for (int j=1; j<args[i].Length; j++)
{
switch (args[i][j])
{
case 'c': bClassName = true; break;
case 'h': bHwnd = true; break;
case 'r': bRect = true; break;
case 'R': bRectangle = true; break;
case 't': bTitle = true; break;
case '?': default: return help();
}
}
}
}
WindowArray itw = new WindowArray();
foreach (int hwnd in itw)
{
if (Win32.IsWindowVisible(hwnd))
{
if (bHwnd)
{
Console.Write("{0:x8}", hwnd);
}
if (bClassName)
{
StringBuilder cname = new StringBuilder(256);
Win32.GetClassName(hwnd, cname, cname.Capacity);
Console.Write(" {0}",cname);
}
if (bRectangle)
{
Rectangle rc = new Rectangle();
Win32.GetWindowRect(hwnd, ref rc);
Console.Write(" {0}",rc);
}
else if (bRect)
{
RECT rc = new RECT();
Win32.GetWindowRect(hwnd, ref rc);
Console.Write(" {0}",rc);
}
if (bTitle)
{
StringBuilder title = new StringBuilder(256);
Win32.GetWindowText(hwnd, title, title.Capacity);
Console.Write(" {0}",title);
}
Console.WriteLine();
}
}
return 0;
}
static int help()
{
Console.WriteLine("ListWin: List toplevel windows.");
Console.WriteLine(" Copyright 2002 Paul DiLascia.");
Console.WriteLine("Format: ListWin [/chrRt]");
Console.WriteLine("Options:");
Console.WriteLine(" /c(lassname) show window class name");
Console.WriteLine(" /h(wnd) show HWNDs");
Console.WriteLine(" /t(itle) show title (caption)");
Console.WriteLine(" /r(ect) show window rect using RECT");
Console.WriteLine(" /R(ectangle) show window rect using Rectangle");
Console.WriteLine("");
return 0;
}
}