30 августа 2006

Контролы от Microsoft снова рулят

Оказывается, нельзя менять стиль стандартного Windows editbox-а. Хотя так естественно было менять стиль multi-line/single-line при обработке WM_SIZE. А не получится.

Такими темпами придется заиспользовтаь что-то типа Scintilla или еще хуже - написать собственный editbox. Давно думаю, что дойдет именно до написания собственного.

Экономия на мелочах?

Некоротые товарищи пытаются сэкономить несколько байт, используя вместо int тип signed char или unsigned char. Их мотивация такая: если у нас числа не больше 127 (255), то зачем использовать четырехбайтный int (на 32 битной платформе), когда можно обойтись всего одним байтом? Экономия - 400%!

Но на деле есть а) выравнивание данных компилятором, б) размер процессорного слова.

Выравнивание может быть настроено на 4 или 8 байт и структура
struct { signed char x, y; } займет 8 или 16 байт, а не 2.

Процессору удобнее общаться с числами определенного размера. 32-битному процессору как правило легче обрабатывать 32-битные данные, для обработки данных меньшего размера скорее всего понадобится столько же такотов, но вероятно потребуются дополнительные префиксы к инструкциям. Например, при вызове функции на стек будет запихиваться 32-разрядное число, хоть значимый там будет всего один байт.

То есть никакой экономии памяти при переводе единичных переменных (не массивов) с int на char скорее всего не будет. Зато возможно будут некоторые проблемы.

Ситуация - чтение данных из текстового потока:
int value;
textstream >> value;


При входном потоке "0" мы получим value == 0. А при замене int на char получим value == 48. Сэкономили несколько байт, а получили лишнюю головную боль.

27 августа 2006

Откуда растут руки у программистов Windows?

Интересно посмотреть в глаза людям, которые писали стандартные контролы в Windows.

Стандартный button позволяет рисовать на кнопке иконку (BS_ICON window style + BM_SETIMAGE message). Все замечательно, пока у вас Windows 2000. Когда у вас Windows XP, то вы понимаете, что кнопка с иконкой не поддерживает темы XP. И поддержку тем можно сделать только вручную. А уж если что-то делать вручную, то лучше уж тогда полностью свою кнопку написать. Чтобы не ожидать очередных подстав со стороны Microsoft.

09 августа 2006

Как закрыть форточку?

WinAPI - вещь для посвященных. Если нужно что-то сделать, мало прочитать документацию (хорошо что хоть она легко доступна). Чтобы что-то сделать правильно, нужно либо долго помучаться, либо перелопатить форумы. Либо и то, и другое.

Задача: закрыть MDI-child окно.
Решение "в лоб": казалось бы, чего проще: DestroyWindow(hWnd)
И даже окно закрывается, на первый взгляд. Но фокус на другое MDI-child окно не переходит, а должен бы. Но это еще не беда. Беда начинается тогда, когда это окно (child) максимизировано. Тогда происходят вообще чудеса: окно закрывается, но от окна в строке меню остаются значки maximize-minimize-close и значок системного меню! Красота, да и только. Если так закрыть несколько окон, то будет полнейший сюрреализм:



Hint: У MDI-child-а обработчик WM_CLOSE по умолчанию закрывает окно (причем делает это грамотно).

Простой вызов SendMessage(hWnd, WM_CLOSE) подходит, но не во всех случаях. Представьте себе, вы пишете обработчик WM_CLOSE, в котором пишете такую конструкцию ;) Да мы просто зациклимся.

В результате у меня получилась подобная конструкция:
bool CMDIFormImpl::Destroy()
{
// This is temporary solution!!!
// todo: replace it by something more suitable

HWND hWnd(m_hWnd);
// call default MDI-child WM_CLOSE handler: it does MDI-destroying better than us
DefWindowProc(WM_CLOSE, 0, 0);
return !::IsWindow(hWnd);
}

01 августа 2006

Убей родителя, и он убъет тебя

В ATL/WTL конструкция delete this; - дело довольно обычное. Так можно в OnFinalMessage() удалить собственный объект.

А теперь представьте ситуацию: WTL-контролы (кнопки, edit box-ы и т.п.) хранятся в контейнере указателей у их Parent-а (диалога, etc.). В своем OnFinalMessage() родитель делает delete this, а его деструктор грохает все объекты WTL-контролов.

Все хорошо до тех пор, пока мы не захотим по нажатию кнопки мыши на контроле закрыть окно родителя. В обработчике контрола по WM_LBUTTONDOWN (скажем, в CControl::OnLButton()) мы вызываем Parent->DestroyWindow() и... падаем по assert-у, так как parent удаляет наш объект, а мы еще не вышли из CControl::OnLButton(), и наш объект еще может понадобится (не в OnLButton(), а в вызвавшем его коде).

Можно, конечно, сделать PostMessage(Parent, WM_CLOSE или WM_DESTROY), и тогда родитель будет удален уже потом, после выхода из CControl::OnLButton(), так как WM_DESTROY дойдет до него не сразу, а просто встанет в очередь. Но иногда нужно грохнуть родителя здесь и сейчас.

Выход из такой ситуации: удалять свой объект должен сам контрол в своем OnFinalMessage(). Правда, родителю не помешает "подчищать" за своими детьми - если контрол был создан как объект, но не успел создасться как окно (в терминах WinAPI) - мало ли по каким причинам - то убить объект контрола должен родитель.

28 июля 2006

Есть ли жизнь после assert()?

assert() - полезный макрос для документирования pre-conditions и отладки кода. Однако, не стоит забывать про release version, когда все эти ассерты превратяться в тыкву. Одно дело, когда условие assert-а зависит от внутреннего состояния программы, другое дело - если от внешних данных. Тут нужно менять assert() на что-то, что выживет в release.

Config = LoadConfig("config.cfg");
assert(Config != NULL);
Config->GetValue(...)


Вот в таком случае явно не место assert-у. Как минимум - throw std::exception("wtf w/config?")

19 июля 2006

Template Template Parameters

То, что параметры шаблона могут не только типами и классами, а еще и шаблонами, я узнал только читая Александреску (нет чтобы стандарт C++ почитать ;)).

В кратце суть шаблонных параметров шаблона:

template <typename T>
class Vector { ... }

template <typename T>
class Deque { ... }

template <template <typename> class Container>
class SomeClass
{
Container<SomeDataType> Data;
}

SomeClass<Vector> VectorObject;
SomeClass<Deque> DequeObject;

13 июля 2006

delete this

Конструкция delete this; выглядит довольно необычно. Выглядит, как будто мы рубим сук, на котором сидим. Однако, все довольно безопасно, если выполнять меры предосторожности (мыть руки перед едой ;)).

Что из себя представляют методы класса? Да это обычные функции, которым неявно передается указатель this.

void vector::remove(ItT it)
на самом деле выглядит примерно как
void ::remove(vector* this, ItT it)

Кстати, int vector::size() const
выглядит как int ::size(const vector* this).

Соответственно, внутри метода можно спокойно удалить какой-то объект, а называется он this или как-то по-другому - это все равно. Главное, не пользоваться этим объектом после удаления. Причем (внимание!) нужно перестать им пользоваться совсем, то есть не только в этом методе после выполнения delete this;, но и там, откуда этот метод вызван.

Но это еще не все предосторожности. Перед тем как сделать delete this; естественно надо убедиться, что объект создан через new, а не на стеке или еще как-то (каким-нибудь нестандартным аллокатором). Как убедиться в этом - вопрос дизайна (design). Можно просто написать в документации: "этот объект нужно создавать через new" или сделать конструкторы приватными и создат static-member-ов для создания объектов. Или вообще использовать boost:shared_ptr, и тогда вместо delete this; будет sp.release(), который удалит объект как ему предписывает назначенный deleter.

Ссылка по теме: ATL's CWindowImpl::OnFinalMessage() crash

04 июля 2006

STL and C++ Standard Library

Многие путают STL и стандартную библиотеку C++. Некоторые думают что это одно и то же.

На самом деле STL - библиотека, разработанная SGI, часть из которой включили в стандарт C++. Уже из этого вытекают два факта:
1. STL ≠ стандартная библиотека C++,
2. не все, что есть в STL, есть в стандартной библиотеке C++.

Если объяснять на пальцах: парни, разрабатывающие стандарт C++, в один день подумали - вон какие хорошие вещи есть в STL, давайте скопируем некоторые из них в наш стандарт. Скопировали не всё, например, в стандартной библиотеке C++ нет алгоритма copy_n, хотя он есть в STL.

Третье. Стандартная библиотека C++ намного шире STL: в ней есть потоки ввода-вывода, auto_ptr, и много чего другого. Так что говоря про стандартную библиотеку не стоит называть ее "STL".

Наиболее полную реализацию STL можно найти в STLPort.

PS. В следующем стандарте в стандартную библиотеку C++ будет включено много вещей из Boost-а, но это не повод называть стандартную библиотеку Boost-ом :)

28 июня 2006

ATL::CWindow

ATL/WTL почему-то не до конца следует принципу «Resource Acquisition Is Initialization». Деструктор класса ATL::CWindow не удаляет окно (не вызывает DestroyWindow()). То есть CWindow - это не окно, это некая прокси для управления окном.

21 июня 2006

Coding Standards: Smart pointers

Добавил в свой Coding Standards раздел про использование указателей.

Интеллектуальные указатели

Любое использование ресурсов должно следовать принципу «Resource Acquisition Is Initialization».

Таким образом, любое выделение памяти через new должно инициализировать умный указатель:
char *Node = new char[Size];
scoped_array<char> Buffer( new char[Size] );

XMLNode* CreateNode(CFile File) { return new XMLNode(File); }
auto_ptr<XMLNode> CreateNode(CFile File) { return new XMLNode(File); }

Это правило относится к любым ресурсам, не только в динамической памяти. Например, если необходимо воспользоваться функциями WinAPI для работы с файлами CreateFile() и CloseFile() необходимо написать класс «оболочку» над этими функциями, который в деструкторе будет закрывать файл.

Можно использовать любые классы их используемых библиотек:
− Standard C++ Library (auto_ptr, fstream, ...)
− boost (shared_ptr, scoped_ptr, ...)
− WTL (CDC, CClientDC, CFont, ...)

Обычные указатели можно использовать только в случае когда выполняются все перечисленные условия:
− указатель не владеет объектом;
− точно известно, что время жизни объекта, на который ссылается указатель, больше чем время жизни указателя.

12 июня 2006

Указатели

Стиль программирования может быть разный. Но по-моему такие конструкции остались далеко в прошлом:
char *Text = new char[TextLength];

Почувствуйте разницу:
boost::scoped_array<char> Text(new char [TextLength]);

29 мая 2006

Pessimization is evil

Многие знают, что ранняя оптимизация - корень всех бед. Но и явная пессимизация - тоже большое зло. Зачем писать явно неоптимально, когда можно написать нормально? Правильно, незачем.

Классический пример:
for
(
iterator It(begin()), End(end());
It != End;
++It
)
{
...
}
Здесь:
1. Использование для инициализации копирующего конструктора вместо присваивания. При написании iterator It = begin() компилятор может создать временный объект, хотя может и соптимизировать.

2. Префиксная форма инкремента. Постфиксный инкремент как правило реализуется через префиксный и временную переменную:
iterator operator++(int)
{
iterator _Tmp = *this;
++*this;
return (_Tmp);
}
В таком случае получаем создание временных объектов, которыми мы не пользуемся. Так что лучше сразу написать префиксную форму.

3. Сохранение вычисленного end(). Если написать условие в виде It != end(), то end() может вычисляться при каждой итерации заново. Поэтому лучше его прокэшировать.

Сеегодня нашел еще одну явную пессимизацию, которую я, оказывается, всегда использовал (WinAPI specific):
DC.FillSolidRect(..., WindowColor);
...
DC.SetBkMode(TRANSPARENT);
DC.SetTextColor(TextColor);
DC.DrawText(...);
То есть заполнял фон, затем писал на нем текст, используя "прозрачный" цвет фона при отрисовке текста.

А теперь подумаем о том, что сейчас у большинства пользователей Windows включен тот стандартный или ClearType антиалиасинг шрифтов. При такой отрисовке многие выводимые пиксели надписи должны отрисовываться с учетом их предыдущего цвета. DrawText() не знает, что мы до этого всю область окна зарисовали каким-то одним цветом. И при отрисовке текста в режиме TRANSPARENT будет считывать то, что уже отрисовано, накладывать на него текст... А ведь можно от этих мучений легко избавиться:
DC.FillSolidRect(..., WindowColor);
...
DC.SetBkMode(OPAQUE);
DC.SetBkColor(WindowColor);
DC.SetTextColor(TextColor);
DC.DrawText(...);

25 мая 2006

C# beats C++

Забавный текст в update history програмки Launchy:
This is the first C++ release. As I became frustrated with C#’s large .NET framework requirements and users lack of desire to install it, I decided to switch back to the faster language. This release is functionally and visually equivalent to the 0.5 C# release. The C# CVS version had some new features that I will soon add to the C++ branch.

Вобщем, Welcome back! ;)

Auto-sized editbox

При создании grid-а понадобилось автоматически расширять редактируемую ячейку (стандартный editbox) по высоте в зависимости от содержимого. A-la редактирование таблиц в Microsoft Word.

Загвоздка вся в том, что editbox - стандартный Windows control, в исходники которого залезть нельзя, а функциональность у него весьма ограниченная.

Задача такая: при изменении содержимого editbox-а установить соответствующую его высоту, чтобы весь текст был виден на экране.

Что имеется у стандартного edit-а:
1. Область format rectangle, которая задает область редактирования. Как правило, чуть меньше чем client area. При изменении размера edit-а, эта format rectangle ставится как ей угодно. (Нет чтобы сделать format margins, которые не меняются...)
2. Сообщение EM_GETLINECOUNT - можно узнать текущее количество строк в контроле.
3. Уведомления EN_UPDATE и EN_CHANGE - можно отслеживать изменения содержимого контрола. EN_UPDATE приходит до прорисовки, EN_CHANGE - после.

При реализации сразу начались проблемы.

Проблема 1. Не приходит уведомление EN_UPDATE. Решилось просто:
EditBox.SendMessage(EM_SETEVENTMASK, 0, ENM_UPDATE);

Проблема 2. После изменения размера контрола происходит изменение format rectangle, приходится возвращать его на место:
EditBox.GetClientRect(ClientRect);
EditBox.SendMessage(EM_SETRECTNP, 0, (LPARAM)(&ClientRect));


Проблема 3. Самая большая проблема. Суть ее такова: необходимая нам высота client area равна КоличествоСтрок * ВысотаСтроки. Количество строк мы получить можем, а вот как высоту строки от edit control-а получить? Долго пытался использовать разумные значения, но никак не выходило правильной отрисовки во всех случаях. Перекопал весь Google.Groups, советов много, но ни один точно не соответсвует тому, как рисует текст editbox.

Магическая формула оказалась такая:
ТребуемаяВысота = КоличествоСтрок * TEXTMETRIC.tmHeight + 2

Нет, представляете - "+ 2". Что это за magic number я не знаю, но его я нашел где-то в ньюсгруппах.

Итого получилось:
void AdjustEditSize()
{
int nLines = (int) EditBox.SendMessage(EM_GETLINECOUNT);
// if (nLines == PrevLines) return;

CRect ClientRect, WinRect;
EditBox.GetClientRect(ClientRect);
EditBox.GetWindowRect(WinRect);
EditBox.SendMessage(EM_GETRECT, 0, (LPARAM)(&FormatRect));

CClientDC DC(EditBox);
DC.SelectFont((HFONT) EditBox.SendMessage(WM_GETFONT));

TEXTMETRIC tm;
DC.GetTextMetrics(&tm);
int LineHeight = tm.tmHeight;

MapWindowPoints(HWND_DESKTOP, EditBox.GetParent(), (LPPOINT)&WinRect, 2);
WinRect.bottom = WinRect.top
+ (WinRect.Height() - ClientRect.Height()) // adding non-client area
+ nLines * LineHeight + 2; // what da hell is "2" here?
EditBox.MoveWindow(WinRect);

// restore fucking format margins. set format rect = client area (i.e. margins=0)
EditBox.GetClientRect(ClientRect);
EditBox.SendMessage(EM_SETRECTNP, 0, (LPARAM)(&ClientRect));
}

15 мая 2006

throw()

Забавная дискуссия получилась по поводу спецификатора throw().

Начали с throw, закончили спором на счет оптимизации.

10 мая 2006

Scintilla

Копаясь в исходниках TortoiseSVN обнаружил хороший edit control, да еще с очень правильной лицензией.

PS. Ложка дегтя.

09 мая 2006

BitBlt does clipping. It does it bad.

Цитата из MSDN: "BitBlt does clipping on the destination DC".
Так вот при этом clipping-е BitBlt забывает про viewport, и режет как будто его нет.
Пришлось извратиться:
void RenderBuffer(HDC PaintDC, const RECT& SourceRect, const POINT& Destination)
{
// BitBlt does clipping on the destination DC.
// But BitBlt's clipping know nothing about view ports :(
// So, we need to set viewport to (0,0)
// and handle original viewport manually when calling BitBlt
POINT OriginalViewport;
::SetViewportOrgEx(PaintDC, 0, 0, &OriginalViewport);

// perform rendering
::BitBlt(PaintDC,
SourceRect.left,
SourceRect.top,
SourceRect.right - SourceRect.left,
SourceRect.bottom - SourceRect.top,
BufferDC,
Destination.x + OriginalViewport.x,
Destination.y + OriginalViewport.y,
SRCCOPY);

// restore original viewport
::SetViewportOrgEx(PaintDC, OriginalViewport.x, OriginalViewport.y, NULL);
}

06 мая 2006

WTL revisited

Вчера я было разочаровался в WTL. Причины были следующие:
1. У него нет класса CIcon. Пришлось хранить HICON и прибивать его (DeleteObject()) собственноручно в деструкторе объекта.
2. У класса CDC нет метода ReleaseDC(). Пришлось делать руками, через WinAPI:
int LogPixelsY;
{
CDCHandle ScreenDC(::GetDC(NULL));
LogPixelsY = GetDeviceCaps(ScreenDC, LOGPIXELSY);
::ReleaseDC(NULL, ScreenDC.Detach());
}

Сегодня почесав репу и пошерстив Гугл понял, что остал от жизни - вышел уже WTL 7.5, а у меня все 7.1 стоит. Скачал, поставил.

1. CIcon появился.
2. Понял, что CDC не предназначен для ReleaseDC(). Для него есть CClientDC:
int LogPixelsY;
{
CClientDC ScreenDC(NULL);
LogPixelsY = GetDeviceCaps(ScreenDC, LOGPIXELSY);
}

Жизнь налаживается...

05 мая 2006

Имя файла и его расширение

Почему-то boost::filesystem::path не понимает wide chars :(
Пришлось писать свои шаблоны:

template <typename StringType>
typename StringType::const_iterator FindFileName(const StringType& FilePath)
{
typename StringType::const_reverse_iterator rend(FilePath.rend()), rit(FilePath.rbegin());
for (; rit != rend; ++rit)
{
switch (*rit)
{
case '/':
case '\\':
return rit.base();
}
}

return FilePath.begin();
}

template <typename StringType>
typename StringType::const_iterator FindFileExtension(const StringType& FilePath)
{
typename StringType::const_reverse_iterator rend(FilePath.rend()), rit(FilePath.rbegin());
for (; rit != rend; ++rit)
{
switch (*rit)
{
case '/':
case '\\':
return FilePath.end();

case '.':
return rit.base();
}
}

return FilePath.end();
}

template <typename StringType>
StringType ExtractFileName(const StringType& FilePath)
{
return StringType(FindFileName(FilePath), FilePath.end());
}

template <typename StringType>
StringType ExtractFileExtension(const StringType& FilePath)
{
return StringType(FindFileExtension(FilePath), FilePath.end());
}

Использование:

std::wstring Path = L"c:\\foo\\bar.xml";
std::wcout << L"file: " << ExtractFileName(Path) << L", ext: " << ExtractFileExtension(Path) << std::endl;


Используемо с basic_string, vector и другими контейнерами.
Понимает как char, так и wchar_t.