Сайт разработчика Александр Климова

/* Моя кошка замечательно разбирается в программировании. Стоит мне объяснить проблему ей - и все становится ясно. */
John Robbins, Debugging Applications, Microsoft Press, 2000

Вызов функций Windows API

Из книги 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.

Вызов функций Windows API, имеющих выходной строковый параметр char*

Предположим, нам необходимо вызвать функцию 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, а не "широких символов".

Вызов функций, требующих struct

Возьмем для примера функцию 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#

Для использования функций, написанных на 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 на прилагаемом к книге диске.

Примеры использования функций API

Вкратце ознакомившись с теорией, перейдем к конкретным примерам. В предыдущих главах я уже неоднократно приводил пример использования функций Windows API для решения различных проблем. Рассмотрим еще несколько полезных советов, которые не вошли в другие главы.

  • Блокировка компьютера - Если вам необходимо блокировать компьютер, то вызовите функцию LockWorkStation. Результат работы примера будет аналогичен нажатию комбинации клавиш Win+L или клавиш Ctrl+Alt+Del с последующим выбором кнопки (или команды меню) Блокировка.
  • Является ли текущий пользователь администратором? - Если необходимо удостовериться, что текущий пользователь имеет права администратора, то можно вызвать функцию IsUserAnAdmin.
  • Мигание заголовка формы - Наверное, вам приходилось видеть, что заголовок окна вдруг начинал мигать, привлекая ваше внимание. Подобный эффект реализуется вызовом функций FlashWindow или FlashWindowsEx.
  • Форматирование дисков - Чтобы вызвать стандартное диалоговое окно форматирования дисков нужно воспользоваться функцией SHFormatDrive
  • Открытие и закрытие лотка привода компакт-дисков - Наверное, при работе с утилитами, прожигающими компакт-диски CD-R и CD-RW, вы замечали, что у них имеется возможность извлекать компакт-диск из привода программным путем. Неплохо бы научиться делать то же самое при помощи C#. Для этого используем функцию mciSendString в связке с специальными командами, которые и позволят нам открывать и закрывать лоток привода компакт-дисков
  • Создание собственного пункта в системном меню - У большинства окон в Windows имеется так называемое системное меню. Внешний вид, как правило, у всех меню одинаков, но иногда попадаются программы, у которых в системном меню имеются свои собственные пункты. Естественно, любого программиста разбирает любопытство — а как реализовать эту функциональность в своей программе. На данный момент .NET Framework не предоставляет такого полезного свойства как Form.SystemMenu или что-то в этом роде. Поэтому придется прибегать к помощи механизма P/Invoke для вызовов функций Windows API. Опытные программисты (особенно имеющие опыт работы с языком С++) знают, что для модификации системного меню используется функция GetSystemMenu, а также вспомогательная функция AppendMenu, которая позволяет добавлять в меню разделители, картинки, галочки и сам текст.
  • Извлечение значков из файлов - Функция ExtractIcon позволяет извлекать значки из ресурсов, которые зашиты в файлах EXE, DLL, CPL и др. Кроме того, функция позволяет подсчитать количество значков, находящихся в файле. В качестве испытуемого файла возьмем динамическую библиотеку shell32.dll, которая имеется в любой версии Windows.
  • Вызов диалогового окна Смена значка - Существует такая функция Windows API как PickIconDlg. Долгое время она была официально не документирована, но, начиная, с Windows 2000 компания Microsoft все-таки выложила описание этой функции на сайте MSDN. Функция PickIconDlg вызывает стандартное диалоговое окно "Смена значка", позволяющее выбрать значок из модуля. Тем самым, мы можем предоставить пользователю возможность самому выбирать нужный значок, после чего и вывести его на форму (или произвести другие операции).
  • Панель задач, кнопка Пуск и часы в области уведомлений - Очень часто программисты хотят получить доступ к стандартным элементам интерфейса Рабочего стола Windows. Например, разработчики хотят получить координаты панели задач, программно нажать на кнопку Пуск, спрятать и показать эту кнопку Пуск и многое другое.
  • Смена обоев Рабочего стола - Если вы хотите периодически менять картинку на своем Рабочем столе, то можете это сделать программным способом прямо из своего приложения. Для смены обоев Рабочего стола вызывается одна функция Windows API SystemParametersInfo.

Заключение

Несмотря на огромное число имеющихся классов .NET Framework, программисту по-прежнему приходится прибегать к вызовам системных функций Windows API. В папке Win32Help на прилагаемом к книге компакт-диске вы найдете демо-версию справочника по функциям Windows API для .NET Framework. Если вам понравится этот справочник, то вы можете приобрести его полную версию на моем сайте.

Приложение

Win32API.cs


// 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);
        // ...продолжайте добавлять нужные функции
    }
}

ListWin.cs


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;
    }
}

Дополнительная информация

Книга C#.Советы программистам

Cтатья Обращение к неуправляемым DLL из C# и корректное завершение процессов из MSDN Август 2002 (Исходный код можно скачать по адресу http://download.microsoft.com/download/msdnmagazine/code/Aug02/WXP/EN-US/CQA0208.exe)

Реклама