Сегодня дали задачку, сделать мастер для удобного выбора даты. Причем, в будущем настраиваемый пользователем. На этом мастере должна присутствовать куча кнопок соответствующая различным периодам. И при наведении мыши на период, должны подсвечиваться все периоды входящие в него. Ну то есть, при выборе года, должны подсветится все месяцы.
![]()
Просто нарисовать подобный мастер в редактор форм просто. Но когда дойдет дело до настроек, будет сложнее. Поэтому сразу делаю лаконичное описание (настройку) всех периодов и автоматический расчет положения всех кнопок.
void ToolEditPeriod::updateItems() {
items.clear();
// Интерфейс представляет собой таблицу. Первые 4 аругмента addItem, это
// положение и размер ячейки. Далее идет рамка ячейки. Далее эффекты анимации:
// tAutoSel - надо подсветить ячейку, если её период входит в выбранную пльзователем ячейку.
// Т.е. когда пользователь выбирает 1 квартал, месяцы Январь, Февраль, Март будут подсвечены.
// tOtherSel - ячейку подсвечивать не надо. Например, элемент ТЕКУЩИЙ КВАРТАЛ не будет подсвечен
// при выборе элемента ТЕКУЩИЙ ГОД. На экране становится слишком много подсвеченных элементов.
// tNoSel -ячейка не участвует в анимации вообще. Её выбор никого не подсвечивает и сама она
// не подсвечивается.
addItem(0, 0, 5, 1, bNone, tNoSel, "Ранее").onClick = [&]() { curYear--; updateItems(); };
int y=1;
auto a = encodeDate(curYear,1,1);
for(int i=0; i<3; i++) {
addItem(0, y, 1, 4, bTop, tAutoSel, i2s(curYear+i), a, incYear(a));
addItem(1, y, 1, 1, bTop, tAutoSel, "1 кв", a, incMonths(a, 3)); // Можно код ниже свернуть в цикл, но так красивее
addItem(2, y, 1, 1, bTop, tAutoSel, "Январь", a, incMonth(a)); a=incMonth(a);
addItem(3, y, 1, 1, bTop, tAutoSel, "Февраль", a, incMonth(a)); a=incMonth(a);
addItem(4, y, 1, 1, bTop, tAutoSel, "Март", a, incMonth(a)); a=incMonth(a);
y++;
addItem(1, y, 1, 1, bNone, tAutoSel, "2 кв", a, incMonths(a, 3));
addItem(2, y, 1, 1, bNone, tAutoSel, "Апрель", a, incMonth(a)); a=incMonth(a);
addItem(3, y, 1, 1, bNone, tAutoSel, "Май", a, incMonth(a)); a=incMonth(a);
addItem(4, y, 1, 1, bNone, tAutoSel, "Июнь", a, incMonth(a)); a=incMonth(a);
y++;
addItem(1, y, 1, 1, bNone, tAutoSel, "3 кв", a, incMonths(a, 3));
addItem(2, y, 1, 1, bNone, tAutoSel, "Июль", a, incMonth(a)); a=incMonth(a);
addItem(3, y, 1, 1, bNone, tAutoSel, "Август", a, incMonth(a)); a=incMonth(a);
addItem(4, y, 1, 1, bNone, tAutoSel, "Сентябрь",a, incMonth(a)); a=incMonth(a);
y++;
addItem(1, y, 1, 1, bNone, tAutoSel, "4 кв", a, incMonths(a, 3));
addItem(2, y, 1, 1, bNone, tAutoSel, "Октябрь", a, incMonth(a)); a=incMonth(a);
addItem(3, y, 1, 1, bNone, tAutoSel, "Ноябрь", a, incMonth(a)); a=incMonth(a);
addItem(4, y, 1, 1, bNone, tAutoSel, "Декабрь", a, incMonth(a)); a=incMonth(a);
y++;
}
addItem(0, y, 5, 1, bTop, tNoSel, "Позднее").onClick = [&]() { curYear++; updateItems(); };
auto d = getCurrentDate();
addItem(5, 0, 1, 1, bLeft, tNoSel, "Всё");
addItem(5, 1, 1, 1, bLeft, tNoSel, "Cегодня", d, incDay(d));
addItem(5, 2, 1, 1, bLeft, tNoSel, "Вчера", decDays(d, 1), d);
addItem(5, 3, 1, 1, bLeft, tNoSel, "Вчера и сегодня", decDays(d, 1), incDay(d) );
addItem(5, 4, 1, 1, bLeft, tNoSel, "За последние 7 дней", decDays(d, 6), incDay(d) );
addItem(5, 5, 1, 1, bLeft, tNoSel, "За последние 14 дней", decDays(d, 13), incDay(d) );
addItem(5, 6, 1, 1, bLeft, tNoSel, "За последние 45 дней", decDays(d, 44), incDay(d) );
addItem(5, 7, 1, 1, bLeft, tNoSel, "За последние 90 дней", decDays(d, 89), incDay(d) );
addItem(5, 8, 1, 1, bLeft, tOtherSel, "Текущий месяц", roundMonth(d), incMonth(roundMonth(d)));
addItem(5, 9, 1, 1, bLeft, tOtherSel, "Текущий квартал", roundMonth3(d), incMonths(roundMonth3(d), 3));
addItem(5, 10, 1, 1, bLeft, tOtherSel, "Текущий год", roundYear(d), incYear(roundYear(d)));
addItem(5, 11, 1, 1, bLeft, tOtherSel, "Прошлый месяц", incMonths(roundMonth(d), -1), roundMonth(d));
addItem(5, 12, 1, 1, bLeft, tOtherSel, "Прошлый квартал", incMonths(roundMonth3(d), -3), roundMonth3(d));
addItem(5, 13, 1, 1, bLeft, tOtherSel, "Прошлый год", incMonths(roundYear(d), -12), roundYear(d));
prepareItems();
}
//-----------------------------------------------------------------------------
// В конструкторе только лишь инициализируем переменные, так как
// при исключении в конструкторе, дестрактор не вызывается.
ToolEditPeriod::ToolEditPeriod() {
hover = 0;
curYear = 0;
}
//-----------------------------------------------------------------------------
// Создание окна
void ToolEditPeriod::create(int wx, int wy, const std::function<void(DateTime, DateTime)>& _onSelect) {
// Сохраняем переменные
onSelect = _onSelect;
// Отображаемый в мастере год. Пользователь может его изменить в процессе работы
curYear = getCurrentDate().y()-1;
// Создаем окно верхнего уровня без рамки. И сразу не показываем, а то оно заберет
// фокус у поля ввода, для которого этот мастер вызывается
createTop(wx, wy, 16, 16, "", bsTopmost|bsTool|bsHidden);
// Окно будет автоматически скрыто, если пользователь кликнет мимо, изменит фокус
// или интерфейс изменится.
hideToolWindow_start0(this, /*canClickSelf=*/true);
// Инициализируем интерфейс
updateItems();
}
Интерфейс описан как таблица. Пока этого хватает и выглядит симпатично. Но если придется переделать, то метод addItem будет просто принимать вместо координат ячеек, координаты пикселей.
Ну а пока, таблица позволяет точно подогнать размер окна и ячеек под размер шрифта. Это делает метод prepare:
void ToolEditPeriod::prepareItems() {
// Оступы
const int padding = 32;
const int borderSize = 1;
const int lineHeight = stdFont.height + 8;
// Определение ширины каждого столбца
vector<int> columns;
int rowsCount = 0;
DCDisplay dc;
for(auto& i : items) {
int tw = (stdFont.width(dc, i.text) + padding) / i.w;
for(int j=i.w, c=i.x; j; --j, ++c) {
if(c >= columns.size()) columns.push_back(tw);
else setMax(columns[c], tw);
}
setMax(rowsCount, i.y+i.h);
}
// Расчет положения и размера ячеек
for(auto& i : items) {
i.gx = borderSize;
for(int j=0; j<i.x; j++)
i.gx += columns[j];
i.gx1 = i.gx - 1;
for(int j=0; j<i.w; j++)
i.gx1 += columns[i.x + j];
i.gy = borderSize + i.y * lineHeight;
i.gy1 = i.gy + i.h * lineHeight - 1;
}
// Определение и изменение ширины окна
int totalWidth = borderSize * 2;
for(auto w : columns)
totalWidth += w;
resizeClient(totalWidth, borderSize * 2 + rowsCount*lineHeight);
invalidate();
// При первом запуске метода prepareItems окно будет отображено. Причем
// фокуса это окно не получит.
show(swShowNa);
}
Функция вывода на экран и обслуживании мыши крайне проста. Но функция вывода будет рашсирена.
void ToolEditPeriod::redraw(DC& dc) {
// Белый фон и серая рамка
dc.clear(0xFFFFFF);
dc.rect(0,0,clientWidth-1,clientHeight-1,1,0x808080);
for(auto& i : items) {
// Подсвечиваем ячейку. Логичка подсветки описана выше
if(hover==&i || (hover && hover->selType!=tNoSel && i.selType==tAutoSel && hover->f <= i.f && hover->t >= i.t))
dc.vertGradient(i.gx, i.gy, i.gx1, i.gy1, CURSOR_BKGND1, CURSOR_BKGND2);
// Текст ячейки выводим по центру
dc.text2(DT_CENTER|DT_VCENTER, i.gx, i.gy, i.gx1, i.gy1, stdFont, 0, i.text);
// Рамка ячейки
if(i.borderType & bLeft) dc.line(i.gx, i.gy, i.gx, i.gy1, 1, 0x808080);
if(i.borderType & bTop) dc.line(i.gx, i.gy, i.gx1, i.gy, 1, 0x808080);
}
}
//-----------------------------------------------------------------------------
// Поиск ячейки по координатам
ToolEditPeriod::Item* ToolEditPeriod::findItem(int x, int y) {
for(auto& i : items)
if(x>=i.gx && y>=i.gy && x<=i.gx1 && y<=i.gy1)
return &i;
return 0;
}
//-----------------------------------------------------------------------------
// Перемещение мыши над объектом
void ToolEditPeriod::wmMouseMove(short x, short y, int keys) {
// Если клавиша мыши нажата, но не обрабатываем перемещение мыши
if(getCaptured()) return;
// Если курсор уже над другой ячейкой, перериосываем интерфейс
auto hover1 = findItem(x, y);
if(hover != hover1) {
hover = hover1;
invalidate();
}
}
//-----------------------------------------------------------------------------
// Ячейка под курсором у нас всегда отмечена, поэтому если курсор вышел за
// переделы нашего окна, надо снять подсветку.
void ToolEditPeriod::wmMouseLeave() {
wmMouseMove(-1, -1, 0);
}
//-----------------------------------------------------------------------------
// Клик мышью
void ToolEditPeriod::wmLButtonDown(short x, short y, int keys) {
hover = findItem(x, y);
if(hover) setCaptured(true);
invalidate();
}
//-----------------------------------------------------------------------------
// Клик мышью не должен отбирать фокус
int ToolEditPeriod::wmMouseActivate(HWND,int,unsigned int) {
return maNoActivate;
}
//-----------------------------------------------------------------------------
// Пользователь отпустил кнопку мыши
void ToolEditPeriod::wmLButtonUp(short x, short y, int keys) {
auto hover1 = hover;
// Перерисовываем в любом случае
invalidate();
// Освобождаем курсор. Ну а если мы его не захватывали, то просто выходим
if(!getCaptured()) return;
releaseCapture();
// Если пользователь убрал мышь с ячейки после нажатия, то выходим
if(hover1==0 || findItem(x, y) != hover1) return;
// Эта ячейка имеет свой обрботчик нажатия
if(hover1->onClick) { hover1->onClick(); return; }
// Возвращаем результат
auto t = hover1->t;
if(t != emptyDateTime) t = decDay(t);
if(onSelect) onSelect(hover1->f, t);
// Закрываем окно
wmClose();
}
//-----------------------------------------------------------------------------
// Закрываем окно
void ToolEditPeriod::wmClose() {
destroy();
deleteIdle();
}
Ну и заголовочный файл
class ToolEditPeriod : public Window {
enum SelType { tAutoSel, tOtherSel, tNoSel };
enum BorderType { bNone=0, bLeft=1, bTop=2 };
class Item {
public:
int x, y, w, h;
int gx, gy, gx1, gy1;
BorderType borderType;
SelType selType;
string text;
DateTime f, t;
std::function<void()> onClick;
};
list<Item> items;
Item* hover;
int curYear;
std::function<void(DateTime, DateTime)> onSelect;
void updateItems();
void prepareItems();
Item* findItem(int x, int y);
Item& addItem(int x, int y, int w, int h, BorderType borderType, SelType selType, cstring text, DateTime f=emptyDateTime, DateTime t=emptyDateTime);
// Сообщения Windows
void redraw(DC& dc);
void wmClose();
void wmLButtonDown(short x, short y, int keys);
void wmLButtonUp(short x, short y, int keys);
void wmMouseMove(short x, short y, int keys);
int wmMouseActivate(HWND,int,unsigned int);
void wmMouseLeave();
public:
ToolEditPeriod();
void create(int x, int y, const std::function<void(DateTime, DateTime)>& onSelect);
~ToolEditPeriod();
};