Category Archives: Разное

Как работает AutoIt Window Info Tool

Последнее время пришлось по работе заниматься написанием небольшого приложения на AutoIt-те и в ходе работы выяснилось, что утилита Autoit Window Info Tool не всегда может извлечь информацию о том или ином элементе управления исследуемого приложения. Тут меня и разобрал интерес, а как эта утилита работает и что в действительности значит получаемая ею информация.

Title для главного окна и Text для элемента управления не требуют особого пояснения.

Class – это название так называемого оконного класса (window class ).

Оконный класс – это структура (WNDCLASSEX), определяющая основные характеристики окна. К ним относятся стиль окна и связанные с окном ресурсы, такие как пиктограмма, курсор, меню и кисть для закрашивания фона. Кроме того, одно из полей структуры содержит адрес оконной процедуры, предназначенной для обработки сообщений, получаемых любым окном данного класса.

Ссылка на оконный класс передается функции CreateWindow (или CreateWindowEx), вызываемой для создания окна.

Использование класса окна позволяет создавать множество окон на основе одного и того же оконного класса и, следовательно, использовать одну и ту же оконную процедуру. Например, все кнопки в программах Windows созданы на основе оконного класса BUTTON. Оконная процедура этого класса, расположенная в системной dll, управляет обработкой сообщений для всех кнопок всех окон. Аналогичные системные классы имеются и для других элементов управления, таких как, например, списки и поля редактирования. В совокупности эти классы называются предопределенными или стандартными оконными классами.

Для главного окна приложение обычно создает собственный оконный класс, инициализируя структуру WNDCLASS (или WNDCLASSEX) и регистрируя этот класс вызовом функции RegisterClass (или RegisterClassEx).

Position – для диалогового окна это (x, y) – координаты относительно левого верхнего угла экрана (можно получить например функцией GetWindowRect), для дочерних окон элементов управления это (x, y) – координаты относительно левого верхнего угла клиентской области содержащего их окна.

Вот фрагмент кода, получающий этот параметр:

    HWND hWnd; // дескриптор исследуемого окна
    HWND hWndRoot = GetAncestor(hWnd, GA_ROOT);
    POINT wndPos; // position окна
    RECT  wndRect;
    GetWindowRect(hWnd, &wndRect);
    int wndWidth  = wndRect.right - wndRect.left;
    int wndHeight = wndRect.bottom - wndRect.top;

    if(hWndRoot == hWnd)
    {
        wndPos.x  = wndRect.left;
        wndPos.y  = wndRect.top;
    }
    else
    {
        WINDOWINFO rootWndInfo = {sizeof(WINDOWINFO)};
        GetWindowInfo(hWndRoot, &rootWndInfo);
        wndPos.x  = wndRect.left - rootWndInfo.rcClient.left;
        wndPos.y  = wndRect.top - rootWndInfo.rcClient.top;
    }

Size – ширина и высота окна в пикселях.

Style и ExStyle – стиль и расширенный стиль окна. Они содержаться в структуре WINDOWINFO значение которой для данного окна можно получить, вызывая функцию GetWindowInfo.

ID – идентификатор дочернего окна, который передается в функцию CreateWindow (или CreateWindowEx) при его создании (см. описание параметра hMenu у этих функций). Этот идентификатор передается в оконную функцию при сообщениях WM_COMMAND и WM_NOTIFY и по нему можно определить элемент управления, к которому относится это сообщение.

Handle – дескриптор окна который возвращает функция CreateWindow (или CreateWindowEx) при своем успешном завершении. Он меняет свое значение от запуска к запуску приложения.

Instance. Для выяснения значения этого параметра проведем следующий эксперимент. Рассмотрим следующее диалоговое окно

Вот фрагмент кода файла ресурсов, который отвечает за его создание:

LANGUAGE LANG_NEUTRAL, SUBLANG_NEUTRAL
IDD_MAIN DIALOG 0, 0, 280, 300
STYLE DS_CENTER | DS_SHELLFONT | WS_CAPTION | WS_VISIBLE | WS_GROUP | WS_THICKFRAME | WS_SYSMENU
EXSTYLE WS_EX_TOPMOST
CAPTION "WinInfo"
MENU IDR_MENU1
FONT 8, "Ms Shell Dlg"
{
    GROUPBOX        "Basic Window Info", IDC_STATIC1, 5, 5, 270, 43
    LTEXT           "Title:", IDC_STATIC, 14, 17, 16, 8, SS_LEFT
    EDITTEXT      IDC_EDIT1, 57, 14, 212, 12, ES_AUTOHSCROLL | ES_READONLY
    LTEXT           "Class:", IDC_STATIC, 15, 32, 20, 8, SS_LEFT
    EDITTEXT       IDC_EDIT2, 57, 30, 212, 12, ES_AUTOHSCROLL | ES_READONLY
    GROUPBOX      "Finder Tool", IDC_STATIC2, 227, 50, 48, 43, BS_NOTIFY
    GROUPBOX      "Basic Control Info", IDC_STATIC, 5, 50, 215, 43
    LTEXT           "Class:", IDC_STATIC, 16, 60, 20, 8, SS_LEFT
    LTEXT           "Instance:", IDC_STATIC, 15, 74, 30, 8, SS_LEFT
    EDITTEXT     IDC_EDIT3, 57, 59, 158, 12, ES_AUTOHSCROLL | ES_READONLY
    EDITTEXT     IDC_EDIT4, 57, 74, 158, 12, ES_AUTOHSCROLL | ES_READONLY
    CONTROL     "", IDC_STATIC3, WC_STATIC, SS_BITMAP | SS_NOTIFY, 233, 61, 32, 25
    CONTROL     "", IDC_TAB1, WC_TABCONTROL, 0, 5, 100, 270, 195
}

За создание четырех текстовых полей Title, Class (в группе Basic Window Info), Class и ID (в Basic Control Info) отвечают выделенные строки. Autoit Window Info Tool для этих полей вернет соответственно значения Instance равные 1, 2, 3 и 4.

Если теперь взять и отредактировать файл ресурсов, поставив строку создающую, скажем, IDC_EDIT4 перед IDC_EDIT1, перекомпилировать проект и посмотреть какие Autoit Window Info Tool возвращает значения, то мы получим:

для IDC_EDIT1 => Instance = 2 , для IDC_EDIT2 => Instance = 3 , для IDC_EDIT3 => Instance = 4 , для IDC_EDIT4 => Instance = 1. То есть, Instance всего лишь указывает на порядок, в котором создаются элементы управления при инициализации приложения. У меня на практике был случай, когда у элементов управления менялся instance при переключении между вкладками в диалоговом окне.

В заключение хочу немного рассказать об алгоритме поиска дочернего окна по оконным координатам указателя мыши. В WinApi для этого есть следующие функции WindowFromPoint , ChildWindowFromPoint , RealChildWindowFromPoint. Но проблема в том, что если, скажем, несколько элементов управления находятся в группирующей их рамке, то по наведению на них мы не получим их дескриптора, а будем получать дескриптор рамки (даже использую RealChildWindowFromPoint у меня не получилось получить дескриптор элемента на котором находился курсор, хотя в документации по RealChildWindowFromPoint и сказано “… For example, if the point is in a transparent area of a groupbox, RealChildWindowFromPoint returns the child window behind a groupbox, whereas ChildWindowFromPoint returns the groupbox.”)

Я делал так. Вызывая WindowFromPoint, находил дескриптор окна, потом для этого окна получал самого верхнего его родителя GetAncestor(hWnd, GA_ROOT) . Потом для этого родителя находил все его дочерние окна. И из них выбирал наименьшее по размерам видимое окно, которому принадлежит точка с координатами указателя мыши. Вот класс, который это реализует.

/* WindowFinder.h */

#pragma once

#include <windows.h>
#include <stack>


#include "Debug.h"

using namespace std;

class WindowFinder
{
    public:
        static HWND findWindow(HWND hWnd, POINT point);
        
    private:
        static BOOL CALLBACK addChildWnd(HWND hWnd, LPARAM lParam);
        static stack<HWND> childWnd;
};

/* WindowFinder.cpp */

#include "WindowFinder.h"

stack<HWND> WindowFinder::childWnd = stack<HWND>();

HWND WindowFinder::findWindow(HWND hWnd, POINT point)
{
    HWND root = GetAncestor(hWnd, GA_ROOT);

    if(root == NULL){
        return NULL;
    }


    HWND findWnd = NULL;
    int  wndSize = INT_MAX;


    EnumChildWindows(root, addChildWnd, 0);

    while(!childWnd.empty())
    {
        HWND testWnd = childWnd.top();
        childWnd.pop();

        RECT testWndRect;

        if(!GetWindowRect(testWnd, &testWndRect))
        {
            DWORD dwError = GetLastError();
            _tprintf(_T("GetWindowRect: dwError = %i\n"), dwError);
            Debug::printErrorMessage(dwError);
            continue;
        }

        if(IsWindowVisible(testWnd) && PtInRect(&testWndRect, point))
        {
            int rectSize = abs((testWndRect.right - testWndRect.left) * (testWndRect.bottom - testWndRect.top));

            if(rectSize < wndSize)
            {
                findWnd = testWnd;
                wndSize = rectSize;
            }
        }
    }

    return findWnd;
}


BOOL WindowFinder::addChildWnd(HWND hWnd, LPARAM lParam)
{
    childWnd.push(hWnd); 
    return TRUE;
}

А вот моя реализация утилиты, которая работает, аналогично Autoit Window Info Tool. На вкладке Wnd Info отображается информация об элементе управления, выбранном при помощи Finder Tool, его непосредственном родителе и корневом родителе. На вкладке All Windows строится дерево из всех окон всех приложений, создавших элементы графического интерфейса на текущий момент. По двойному клику на узле дерева появляется окно с подробной информацией об элементе.

Вот и все, спасибо за внимание. :)

Компилятор Visual Studio (начало работы)

Это пока что лишь черновик

Цель данной статьи осветить некоторые тонкие моменты компиляции c++ приложения компилятором Visual Studio. Все примеры будут компилироваться из командной строки (bat файлами) это позволит лучше понять происходящее. В примерах я использую Visual Studio Express 2012 for Windows Desktop.

Рассмотрим следующий простой пример консольного приложения Windows в котором печатаются все параметры, передаваемые через командную строку, и переменные окружения (envp — это указатель на массив, содержащий переменные окружения с их значениями, разделенные знаком равенства (=)):

#include <windows.h> 
#include <tchar.h>

int _tmain(int argc, TCHAR* argv[], TCHAR* envp[])
{
    _tprintf(TEXT("argc = %d\n"), argc); 
    for(int i = 0; i < argc; ++i) 
    { 
        _tprintf(TEXT("argv[%d] = \"%s\"\n"), i, argv[i]); 
    } 

    int n = 0;
    while(envp[n] != NULL)
    {
        _tprintf(TEXT("envp[%d] = \"%s\"\n"), n, envp[n]);
        n++;
    }
    return 0;
}

.bat файл, компилирующий этот пример, имеет вид:

set "vc_path=C:\Program Files\Microsoft Visual Studio 11.0\VC\"
call "%vc_path%vcvarsall.bat" x86

set "bin=bin\"
set "src=src\"
set "progname=simple"

"%vc_path%bin\cl.exe" /c /ZI /nologo /W3 /Od /Oy- /D "WIN32" /D "_DEBUG" /D "_WINDOWS" /D "_UNICODE" /D "UNICODE" ^
                      /Gm /EHsc /RTC1 /MDd /GS /fp:precise /Zc:wchar_t /Zc:forScope /Fo"%bin%%progname%.obj" ^
                      /Fd"%bin%%progname%.pdb" /Gd /TP /analyze- /errorReport:prompt %src%%progname%.cpp

"%vc_path%bin\link.exe" /ERRORREPORT:PROMPT /OUT:"%bin%%progname%.exe" /INCREMENTAL /NOLOGO kernel32.lib user32.lib ^
                       gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib ^
                       odbc32.lib odbccp32.lib /MANIFEST /MANIFESTUAC:"level='asInvoker' uiAccess='false'" ^
                       /manifest:embed /DEBUG /PDB:"%bin%%progname%.pdb" /TLBID:1 /DYNAMICBASE /NXCOMPAT ^
                       /IMPLIB:"%bin%%progname%.lib" /MACHINE:X86 %bin%%progname%.obj 

pause

При этом предполагается следующее расположение файлов

Во всех Windows-приложениях должна быть входная функция, за реализацию которой отвечаете вы. Существует две такие функции:

    int WINAPI _tWinMain(
       HINSTANCE hInstanceExe,
       HINSTANCE,
       PTSTR pszCmdLine,
       int nCmdShow);

    int _tmain(
        int argc,
        TCHAR *argv[],
        TCHAR *envp[]);

_tWinMain и _tmain это в действительности макросы, которые раскрываются как WinMain или wWinMain для _tWinMain и main или wmain для _tmain в зависимости от того используется Unicode или нет.

На самом деле входная функция операционной системой не вызывается. Вместо этого происходит обращение к стартовой функции из библиотеки C/C++, заданной во время компоновки параметром -entry: командной строки. Она инициализирует библиотеку С/С++, чтобы можно было вызывать такие функции, как malloc и free, а также обеспечивает корректное создание любых объявленных вами глобальных и статических С++-объектов до того, как начинается выполнение вашего кода. В следующей таблице показано, в каких случаях реализуются те или иные входные функции.

Типы приложений и соответствующие им входные функции

Тип приложения Входная функция Стартовая функция, встраиваемая в исполняемый файл
GUI-приложение, работающее с ANSI-символами и строками _tWinMain (WinMain) WinMainCRTStartup
GUI-приложение, работающее с Unicode-символами и строками _tWinMain (wWinMain) wWinMainCRTStartup
CUI-приложение, работающее с ANSI-символами и строками _tmain (main) mainCRTStartup
CUI-приложение, работающее с Unicode-символами и строками _tmain (wmain) wmainCRTStartup

Компоновщик отвечает за выбор подходящей стартовой функции из библиотеки С/С++ при компоновке исполняемого файла. Если задан ключ /SUBSYSTEM:WINDOWS, компоновщик ищет в коде функцию WinMain или wWinMain. Если этих функций нет, компоновщик сообщает об ошибке "unresolved external symbol". В противном случае компоновщик выбирает WinMainCRTStartup или wWinMainCRTStartup, соответственно.

Аналогичным образом, если задан ключ /SUBSYSTEM:CONSOLE, компоновщик ищет в коде функцию main или wmain и выбирает соответственно mainCRTStartup или wmainCRTStartup; если в коде нет ни main, ни wmain, сообщается о той же ошибке – "unresolved external symbol".

Но не многие знают, что в проекте можно вообще не указывать ключ /SUBSYSTEM компоновщика. Если вы так и сделаете, компоновщик будет сам определять подсистему для вашего приложения. При компоновке он проверит, какая из четырех функций ( WinMain, wWinMain, main или wmain ) присутствует в вашем коде, и на основании этого выбирет подсистему и стартовую функцию из библиотеки С/С++.

Теперь несколько замечаний по .bat файлу и ключам компилятора и компоновщика

При создании .bat файла нужно четко следить за тем, чтобы создаваемый текстовый файл имел формат завершения строки в стиле Windows или Unix, но не Mac, иначе bat файл просто не будет запускаться.

Для нормальной работы компилятора перед вызовом cl.exe или link.exe необходимо вызвать call “%vc_path%vcvarsall.bat” x86. Этот bat файл инициализирует переменные окружения INCLUDE, LIB, LIBPATH, PATH и некоторые другие необходимые для работы cl.exe и link.exe. Например, в моем случае было

set "INCLUDE=C:\Program Files\Microsoft Visual Studio 11.0\VC\INCLUDE;C:\Program Files\Windows Kits\8.0\include\shared;C:\Program Files\Windows Kits\8.0\include\um;C:\Program Files\Windows Kits\8.0\include\winrt;"

set "LIB=C:\Program Files\Microsoft Visual Studio 11.0\VC\LIB;C:\Program Files\Windows Kits\8.0\lib\win8\um\x86;"

set "LIBPATH=C:\Windows\Microsoft.NET\Framework\v4.0.30319;C:\Windows\Microsoft.NET\Framework\v3.5;C:\Program Files\Microsoft Visual Studio 11.0\VC\LIB;C:\Program Files\Windows Kits\8.0\References\CommonConfiguration\Neutral;C:\Program Files\Microsoft SDKs\Windows\v8.0\ExtensionSDKs\Microsoft.VCLibs\11.0\References\CommonConfiguration\neutral;"

При вызове cl.exe использовались следующие ключи:

  • – компиляция без связывания
  • /ZI – компилятор включает отладочную информацию в базу данных приложения (только для x86)
  • /nologo – подавляет отображение информации о компиляторе
  • /W3 – устанавливает уровень предупреждений компилятора на 3 уровень
  • /Od – отключает оптимизацию (так как /Od предотвращает перемещение кода, установка этого ключа облегчает процесс отладки)
  • /Oy – предотвращает создание указателей фрейма в стеке вызова, /Oy- отключает такое поведение (только для x86)
  • /D “_DEBUG” – определяется при компиляции с ключами /LDd, /MDd и /MTd
  • /D “_WINDOWS” – определяет, что целевая OS – Windows
  • /D “UNICODE” – указывает компилятору на необходимость использовать в приложении версии Win API функций для работы с Unicode
    Например, вот фрагмент из WinUser.h:
        #ifdef UNICODE
        #define CreateWindowEx CreateWindowExW
        #else
        #define CreateWindowEx CreateWindowExA
        #endif
    
    То есть, если мы при компиляции указываем /D “UNICODE” и в своем коде вызываем CreateWindowEx, то на самом деле происходит обращение к CreateWindowExW
  • /D “_UNICODE” – подобно UNICODE указывает на использование в приложении версий функций из библиотеки С для работы с Unicode строками. Так, например, в заголовочном файле TChar.h можно найти определение следующего макроса:
        #ifdef  _UNICODE
        #define  _tcslen  wcslen
        #else
        #define  _tcslen  strlen
        #endif
    
    Теперь при вызове _tcslen и определенном _UNICODE вызов разрешается в wcslen, в противном случае – в strlen. По умолчанию в новых С++-проектах Visual Studio определен _UNICODE (как и UNICODE)
  • /Gm – включает минимальную перекомпиляцию, которая позволяет перекомпилировать только файлы с измененным исходным кодом
  • /EHsc – перехватываются только С++ исключения и гарантируется, что внешние С функции, никогда не выбросят С++ исключение
  • /RTC (Run-Time Error Checks) (Проверка ошибок времени выполнения)

    /RTC1 – эквивалентно /RTCsu

    /RTCs – включает проверку стекового фрейма времени выполнения, что означает:

    • инициализацию локальных переменных ненулевыми значениями. Это позволяет идентифицировать баги, которые не проявляются в отладочной сборке. Больщая вероятность, что переменная в стеке будет оставаться нулевой в отладочной сборке нежели в рилизной сборке, так как компилятор оптимизирует стек переменных в рилиной сборке. Будучи однажды использованной память отведенная под стек не обнуляется компилятором. По этому, последующие не инициализированные переменные в стеке будут содержать значения оставшееся от предыдущего использования этой области памяти.
    • Проверка выхода за границы локальных переменных, таких как массивы. /RTCs не определяет выход за границы при обращении к памяти явившейся результатом выравнивания компилятором положения структуры в памяти. Такое может произойти при использовании выравнивания (С++ ), /Zp или pack или, если вы распологаете элементы структуры в таком порядке, который вынуждает компилятор вставлять отступы.
    • Проверка указателя стека, что позволяет определить разрушение указателя стека. Разрушение указателя стека может произойти при несоответствии типов вызова. Например, используя указатель на функцию, вы можите вызвать функцию из DLL, которая экспортируется как __stdcall, но вы определили указатель на функцию как __cdecl.

    Замечание:
    стековый фрейм (функции) – область памяти, выделяемая всякий раз, когда вызывается функция, предназначается для временного хранения аргументов и локальных переменных функции.

    /RTCu – заставляет компилятор выдавать предупреждения, когда переменная используется без инициализации. Например, команда, которая генерирует предупреждение (4-го уровня) С4701 может также генерировать ошибку времени выполнения при установленном ключе /RTCu. Любые команды, которые генерируют предупреждения компилятора (уровня 1 и 4) С4700, будут также генерировать ошибку времени выполнения при установленном ключе /RTCu.

    Тем не менее, рассмотрим следующий фрагмент кода:

        int a, *b, c;
        if ( 1 ){
               b = &a;
        }
        c = a;  // No run-time error with /RTCu
    

    Если переменная могла быть проинициализирована, установка ключа /RTCu не приведет к предупреждению времени исполнения. В сущности, вы можете проинициализировать переменную, выполняя операцию взятия ее адреса. В последнем фрагменте кода оператор & работает как оператор присваивания. (???)

SSH Tunnel в HeidiSQL

Решил написать эту статью под впечатлением нескольких часов проведенных в поисках проблем с подключением к базе через SSH Tunnel в HeidiSQL.

Если есть доступ к серверу по SSH , то гораздо удобней работать с базой через HeidiSQL, чем скажем через phpMyAdmin.

Итак настройка подключения:
1) вкладка Settings: heidisql settings
Здесь указываем параметры подключения к базе из конфига CMS.

2) вкладка SSH tunnel: heidisql ssh tunnel
plink.exe можно взять тут, и дальше host, port, user, password от ssh.

Если после этого вам удастся подключиться к базе, значит вам повезло и можно дальше не читать.

У меня была ошибка:
SQL Error (2003): Can't connect to MySQL server on 'localhost' (10061)

Запускаем консоль, заходим в папку где лежит plink.exe и вводим:

  > plink.exe  -ssh  server_user@server_host  -pw  “server_pass”  -P 22 -N -L  3307:localhost:3306 

При первом соединении plink спрашивает доверяем ли мы серверу с данным key fingerprint, соглашаемся. После чего plink заносит этот ключ в реестр windows в HKEY_CURRENT_USER\Software\SimonTatham\PuTTY\SshHostKeys ( что можно увидеть используя утилиту windows regedit )

После этого идем в HeidiSQL и пробуем конектится, в моем случае все прошло успешно.

Если у вас установлен ssh клиент putty (или WinSCP, с которым иногда устанавливается и putty) то этаже ошибка может возникать из за того что plink использует настройки заданные в putty. В этом случае может помочь следующее:
1) Запускаем putty и в его настройках указываем Connect type – любой не ssh ( например Telnet ), сохраняем настройки.
2) Идем в HeidiSQL и на вкладке SSH tunnel в поле plink.exe location через пробел указываем флаг -ssh , сохраняемся, пробуем соединиться.

Вот и все, всем удачи и поменьше таких проблем :) .

Эта проблема также обсуждалась тут.