Пишем простое консольное приложение на чистом WinAPI

18 ноября 2013

В прошлой заметке мы настроили окружение для программирования на WinAPI и даже написали программку, показывающую окно с сообщением. Сегодня мы напишем простое консольное приложение, а также научимся делать кое-какие несложные операции над строками и даже коснемся вопроса управления памятью.

Как помните, в прошлый раз мы получили архив с шаблоном минималистического проекта, свободного от отладочной информации, системы времени исполнения языка C++ и так далее. Создадим на его основе новый проект с именем Console. Чтобы операционная система могла отличить консольные приложения от приложений с графическим интерфейсом, тип приложения кодируется прямо в исполняемом файле. В шаблоне приложения сейчас указан тип GUI, а нам с вами хотелось бы сменить его на CLI. Для этого откройте свойства проекта (Alt+F7) и смените значение в поле Linker → System → SubSustem на «Console». Теперь при запуске программы будет появляться черное окно. Половина дела сделана, осталось только научиться что-нибудь в это окно выводить, а также читать из него ввод пользователя.

Чтение и запись осуществляются с помощью процедур ReadConsole и WriteConsole соответственно:

ReadConsole(hStdin, &szMsg, dwSize, &dwCount, NULL);
WriteConsole(hStdout, &szMsg, dwSize, &dwCount, NULL);

Процедуры имеют одинаковое количество и типы аргументов. Слева направо — (1) хэндл потока stdin, stdout или stderr, он же дескриптор или handle, (2) указатель на буфер со строкой, (3) размер буфера, (4) указатель на двойное слово, в которое будет записано реальное количество прочитанных или записанных символов и (5) параметр, зарезервированный на будущее, должен быть NULL. В случае ошибки процедуры возвращают нулевое значение, а в случае успеха — значение, отличное от нуля. Узнать подробности о возникшей ошибке можно с помощью процедуры GetLastError (это своего рода аналог переменной errno). Однако на практике все забивают на коды возврата этих процедур, потому что никто не хочет писать десятки вложенных if’ов.

Вроде не сложно, но как получить дескриптор потока ввода (stdin) или вывода (stdout)? Для этого предназначена процедура GetStdHandle:

HANDLE hStdin = GetStdHandle(STD_INPUT_HANDLE);
HANDLE hStdout = GetStdHandle(STD_OUTPUT_HANDLE);

Как нетрудно догадаться, процедура принимает константу, определяющую, какой хэндл мы хотим получить, и возвращает этот хэндл.

Отлично, теперь мы умеем читать и писать! Для полного счастья не хватает разве что возможности работать с аргументами командной строки. К счастью, и это нетрудно:

INT nArgs = 0;
LPWSTR lpCommandLine = GetCommandLine();
LPWSTR* lpArgs = CommandLineToArgvW(lpCommandLine, &nArgs);
// ...
LocalFree(lpArgs);

Процедура GetCommandLine возвращает указатель на строку, содержащую имя текущей программы и ее аргументы, а процедура CommandLineToArgvW разбивает эту строку на аргументы, возвращая массив указателей на эти аргументы (lpArgs) и количество аргументов (записывается в nArgs, указатель на который передается вторым аргументом). Память, выделенная под lpArgs, освобождается с помощью процедуры LocalFree.

К этому моменту вы, конечно же, обратили внимание, что здесь используются довольно странные типы (INT, HANDLE, LPWSTR, …). В основном эти типы представляют собой обычные сишные типы, переименованные с помощью typedef. Например, вместо INT можно спокойно писать int, а вместо TCHAR — wchar_t. Что же до используемого соглашения об именовании переменных (nArgs, hStdin, lpCommandLine, …), то это та самая венгерская нотация. Идея заключается в том, чтобы кодировать тип переменной в ее имени. Например, если имя переменной начинается с h, sz, dw или lp, то ее тип, соответственно, является хэндлом, строкой, двойным словом или указателем. Подробности можно найти здесь. Есть аргументы как за, так и против венгерской нотации. Здесь и далее мы будем использовать это соглашение и имена типов тупо по той причине, что они используются в MSDN и Visual Studio, а также являются внутренним стандартом Microsoft. Другими словам, так уж принято писать на WinAPI и в большинстве исходников, которые вы найдете, используется именно такие соглашения. Кто мы такие, чтобы нарушать традиции и осуждать чью-то культуру?

Давайте-ка уже перейдем от слов к делу и напишем какую-нибудь программу:

#include <windows.h>

#define STRLEN(x) (sizeof(x)/sizeof(TCHAR) - 1)
const TCHAR szMsg[] = L"What's your name?\n";

void ChangeTextColor(HANDLE hSomeHandle) {
  INT nArgs = 0;
  LPWSTR lpCommandLine = GetCommandLine();
  LPWSTR* lpArgs = CommandLineToArgvW(lpCommandLine, &nArgs);
  if(nArgs >= 2 && 0 == lstrcmpi(lpArgs[1], L"green")) {
    SetConsoleTextAttribute(hSomeHandle, FOREGROUND_GREEN);
  }
  LocalFree(lpArgs);
}

int main() {
  HANDLE hStdout = GetStdHandle(STD_OUTPUT_HANDLE);
  HANDLE hStdin = GetStdHandle(STD_INPUT_HANDLE);
  TCHAR szName[16];
  TCHAR szResp[32];
  DWORD dwCount = 0;

  ChangeTextColor(hStdout);

  WriteConsole(hStdout, &szMsg, STRLEN(szMsg), &dwCount, NULL);
  ReadConsole(hStdin, &szName, STRLEN(szName), &dwCount, NULL);

  if(dwCount >= 2 &&
    '\n' == szName[dwCount-1] &&
    '\r' == szName[dwCount-2]) {
    szName[dwCount-2] = '\0';
  } else if(dwCount > 0) {
    szName[dwCount] = '\0';
  }

  wsprintf(szResp, L"Hello, %s!\n", szName);
  WriteConsole(hStdout, &szResp, lstrlen(szResp), &dwCount, NULL);

  ExitProcess(0);
}

Вы, конечно же, сразу поняли, что эта программа спрашивает имя пользователя, а затем говорит ему «привет». Также программа проверяет переданные аргументы, и если первым аргументом была передана строка «green» (буквы могут быть в любом регистре), то цвет выводимого текста меняется на зеленый. Делается это с помощью процедуры SetConsoleTextAttribute. Для сравнения строк используется процедура lstrcmpi, которая является полным аналогом сишной процедуры strcmpi. Также есть процедуры WinAPI lstrcmp, lstrcpy, lstrncpy, lstrcat, lstrncat, lstrlen и wsprintf. Последние две процедуры также используется в приведенной программе. Как несложно догадаться, они являются аналогами strlen и sprintf.

В общем-то, это все. Как и в прошлый раз, программа спокойно компилируется с помощью MinGW и запускается под Wine, вот правда цвет выводимого текста не меняет. В качестве домашнего задания можете попробовать исправьте программу так, чтобы перед своим завершением она возвращала цвет текста к прежнему состоянию. Здесь вам поможет процедура GetConsoleScreenBufferInfo. А если эта задача покажется вам слишком простой, попробуйте перевести программу на русский язык. Учтите, что здесь придется потрудиться!

Как всегда, буду рад вашим вопросам и дополнениям.

Дополнение: Учимся работать с файлами через Windows API

Метки: , .


Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.