31 октября 2006

Отбросьте ваши тени в сторону

В минимальных требованиях к окружению (харду и софту) разрабатываемой мною системы стоит "Windows 2000 и выше". Так что приходится тестировать систему под Win2k. О некоторых таких приключениях я уже писал, а недавно Win2k ввел меня в очередной ступор.

При нажатии на кнопку, раскрывающую список combobox'а, всегда вылетаем по assert(hwnd == NULL). Под WinXP все отлично. Тут надо заметить, что Win2k стоит у меня на виртуальной машине на ноуте. На эту виртуальную машину для отладки пришлось поставить Visual Studio (предварительно установив кучу всяких IE Service Pack, .NET, etc, которые требовал VS).

Проект компилировался на виртуальной машине минут 30-60!!! Оказалось что не может зарегистрироваться оконный класс раскрывающегося списка combobox-а, причем GetLastError() выдает "Недопустимый параметр". Какой параметр - не говорит. Кстати, ради добавления этого самого GetLastError() в код его пришлось перекомпилировать - а это опять полчаса дикого жужжания вентилятора процессора (на ноуте у меня Celeron M 1.4).

Класс не регистрировался из-за стиля CS_DROPSHADOW, которого видимо нет на Win2000. Вот так все оказалось просто. А времени на отладку ушло просто офигеть сколько.

PS. Вообще надо памятник поставить изобретателям assert()-ов. Мега-полезная вещь.

30 октября 2006

Как легко и не принужденно держать себя за волосы

В моем проекте в части GUI написаны собственные классы типа CEdit, CButton и etc, которые имеют интерфейсы, используемые бизнес-логикой. Непосредственно реализация этих контролов представлена в классах типа CEditImpl, CButtonImpl и т.п., которые наследуются от ATL::CWindow. Эти C*Impl содержатся в виде private членов внутри соответствующих C*.

Все было отлично. Но вот по событию "нажатие кнопки", приходящему синхронно от CButton в бизнес-логику, эта бизнес-логика закрывает окно, в котором содержится кнопка и все рушится по assert-у в недрах ATL::CWindow. Фигня вся в том, что все это происходит в обработчике WM_LBUTTONDOWN, а закрытие окна ведет к удалению объекта этого самого окна, которое грохает коллекцию его контролов (CEdit, CButton, ...), они в свою очередь грохают объекты CEditImpl, CButtonImpl, ... А деструктор класса ATL::CWindow (от него унаследованы все CButtonImpl) ругается на то, что виндовый контрол "кнопка" еще не уничтожен - внутри его обработчика WM_LBUTTONDOWN мы сейчас сидим.

В ATL/WTL безопасно можно грохать оконный объект в виртуальной OnFinalMessage(), то есть объект CButtonImpl должен пережить CButton, а в CButtonImpl::OnFinalMessage() прибить себя. Но все же CButton тоже должен уметь убивать CButtonImpl, например если виндовый контрол еще не был создан - в таком случае до CButtonImpl::OnFinalMessage() дело не дойдет.

Вобщем было много головной боли по поводу того кто и когда должен грохнуть объект CButtonImpl: то ли сам CButtonImpl, то ли CButton, они должны между собой согласовать действия чтобы:
1. не упать по assert-у в недрах деструктора ATL::CWindow
2. все же удалить CButtonImpl
3. не удалить его несколько раз ;)

Вначале я написал море кода, которое спотыкалось то тут, то там. Но вскоре удалил его нафиг, так как понял что изобретаю велосипед.

Решение получилось примерно такое:
class CButtonImpl : ATL::CWindow...
{
public:
void OnFinalMessage() { SharedPointer.release(); }

boost::shared_ptr<CButtonImpl> SharedPointer;
}

class CButton
{
public:
CButton() : _ButtonImpl(new CButtonImpl)
{
}

void Create()
{
_Button->Create();
_Button->SharedPointer = _Button;
}

private:
boost::shared_ptr<CButtonImpl> _ButtonImpl;
};

То есть условно говоря, CButtonImpl держит себя за волосы пока он существует как виндовый контрол.

Ссылка по теме: delete this

26 октября 2006

Easy constructors

Иногда необходимо расширить поведение существующего класса - добавить метод, изменить метод.

В простейшем случае все выглядит достаточно просто:
class Foo : public Bar
{
public:
void NewMember();
}

Однако в таком случае унаследованы все публичные методы, но не конструкторы Bar (разве что кроме конструктора по умолчанию). Вручную "наследовать" все конструкторы может быть довольно утомительно, поможет парочка шаблонных конструкторов:
class Foo : public Bar
{
public:
Foo() : Bar() {}
template <typename T> Foo(T t) : Bar(t) {}
template <typename T1, typename T2> Foo(T1 t1, T2 t2) : Bar(t1, t2) {}
...

void NewMember();
}

11 октября 2006

Precompiled headers fun

Сегодня упорно боролся с компилятором. Ну не видет он некоторые классы и всё тут, хотя нужные header-файлы указаны.
#include "Event.h"
#include "Control.h"
Event event;
Вот на последней строчке он и ругается - не знаю я ваш Event и все тут.

После некоторого времени ковыряний до мнея дошла замечательная мысль - файл Control.h указан как файл, по которому нужно использовать precompiled header.

То есть встречая Control.h компилятор грузит precompiled header и напрочь забывает про header-ы, которые были до него.

Поставил #include "Control.h" первой строкой - компилятор стал доволен.

10 октября 2006

XP Themes and Windows 2000

В текущем у меня проекте активно используются XP-темы и для простоты их использования я создал небольшую иерархию классов:
CSkin - CBaseSkin - CXPThemeSkin

CSkin представляет из себя интерфейс, CBaseSkin рисует без XP-тем (то есть вполне должен выполняться на Windows 2000), CXPThemeSkin соответственно рисует с помощью XP-тем (если не удается - рисует с помощью своего базового класса - CBaseSkin).

CXPThemeSkin использует всякие OpenThemeData(), CloseThemeData() из uxtheme.dll, CBaseSkin ничего этого не использует.

При старте программа определяет версию Windows и при WinXP создает объект CXPThemeSkin, в противном случае - CBaseSkin.

И я наивно полагал, что это будет работать на Windows 2000. А вчера поставил Win2k на виртуальную машину и... разочаровался - при старте приложение вылетало с ошибкой "Unable to find uxtheme.dll". Самое интересное, что дело не доходило даже до WinMain() и создания объекта CxxxSkin. То есть XP-темы еще не использовались, а приложение уже вылетало.

При разборе полетов выяснил, что любое упоминание в программе функций из uxtheme.dll приводит к ее загрузке uxtheme.dll, даже такой безобидный код:
void foo()
{
HTHEME Theme = OpenThemeData(NULL, NULL);
}

void WindMain(...)
{
return 0; // do not call foo()
}

Я уж было предполагал, что придется отказаться от простых вызовов OpenThemeData() и подобных, и переписать весь код с использованием LoadLibrary("uxtheme.dll"), вручную получением указателей на функции типа OpenThemeData().

Оказалось, все гораздо проще: достаточно указать в настройках проекта
Linker/Input/Delay Loaded DLLs: uxtheme.dll

01 октября 2006

Edit_SelectAll()

Казалось бы, простая задача - при получении фокуса edit box'ом выделить в нем весь текст. У стандартного edit-а в Windows есть даже такое сообщение EM_SETSEL, которое позволяет выделить любую часть текста. Можно и весь текст выделить. Только при любом выделении текста через EM_SETSEL курсор встает после выделения. Таким образом если текст в edit-е введен длинный - то мы увидим хвост этого текста, а не начало. А такой вариант - полный идиотизм: например, при переходе в ячейку, в которую введено "Иванов Петр Григорьевич" мы увидим [етр Григорьевич] вместо [Иванов Петр Гр], хотя последний вариант намного информативный. К слову сказать, в Microsoft Office сделано правильно, но там свои контролы, а у нас - стандартный edit.

Облазив DejaNews Google.Groups я понял, что цивилизоанного способа попросить стандартный edit поставить курсор в начале выделения люди так и не нашли. Пришлось написать свой трехколесный велосипед:
1. Ставим курсор в конец текста
2. Нажимаем Shift-Home

Как я не люблю такие извращения... Давно пора написать свой edit control или перейти на Scintilla.
void Edit_SelectAll(HWND Edit)
{
ATLASSERT(::IsWindow(Edit));

// Move cursor to the end of text
int Len = ::GetWindowTextLength(Edit);
::SendMessage(Edit, EM_SETSEL, Len, Len);

// Set [Shift] key state as pressed
BYTE KeyboardState[256];
::GetKeyboardState(KeyboardState);
BYTE ShiftState = KeyboardState[VK_SHIFT];
KeyboardState[VK_SHIFT] = ShiftState | 0x80;
::SetKeyboardState(KeyboardState);

// Press [Home] key
::SendMessage(Edit, WM_KEYDOWN, VK_HOME, 0x01470001);

// Restore [Shift] key state
KeyboardState[VK_SHIFT] = ShiftState;
::SetKeyboardState(KeyboardState);
}