#include "alfpriv.h" // Win32s doesn't like using the original message numbers for custom messages #define ALF_CB_INSERTSTRING (ALF_WM__BASE + 200) #define ALF_CB_ADDSTRING (ALF_WM__BASE + 201) #define ALF_CB_GETCURSEL (ALF_WM__BASE + 202) #define ALF_CB_SETCURSEL (ALF_WM__BASE + 203) #define ALF_CB_GETLBTEXTLEN (ALF_WM__BASE + 204) #define ALF_CB_GETLBTEXT (ALF_WM__BASE + 205) #define ALF_CB_DELETESTRING (ALF_WM__BASE + 206) #define ALF_CB_GETSTYLE (ALF_WM__BASE + 207) #define ALF_CB_FINDSTRINGEXACT (ALF_WM__BASE + 208) TCHAR *_alf_comboClass = NULL; typedef struct { DWORD comboStyle; } ALFComboCreateParams; static int ALF__ComboItemHeight(HWND hwnd) { int height = 0; HDC hDc = GetDC(hwnd); HFONT font = (HFONT)SendMessage(hwnd, WM_GETFONT, 0, 0); if (font) { HFONT oldfont = SelectFont(hDc, font); TEXTMETRIC tm; ZeroMemory(&tm, sizeof(tm)); if (GetTextMetrics(hDc, &tm)) { height = tm.tmHeight; } SelectFont(hDc, oldfont); } if (LOBYTE(LOWORD(GetVersion())) < 4) { // baseline alignment for NT3.x/Win32s // ideally, this would only be applied for the ODS_COMOBOXEDIT state, // but that state is not sent pre-Win95. height += 2; } ReleaseDC(hwnd, hDc); return height; } static LRESULT CALLBACK ALF__ComboWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { HWND hwndChild = (HWND)GetWindowLongPtr(hwnd, 0); if (uMsg == WM_CREATE) { ALFComboCreateParams *params = (ALFComboCreateParams*)((CREATESTRUCT*)lParam)->lpCreateParams; DWORD comboStyle = params->comboStyle | CBS_OWNERDRAWFIXED | CBS_HASSTRINGS; HWND hwndCombo = CreateWindowEx(0, TEXT("COMBOBOX"), TEXT(""), WS_CHILD | WS_VISIBLE | WS_VSCROLL | WS_TABSTOP | comboStyle, 0, 0, 100, 200 /* FIXME needed for commctl32 v5, what is the best value here? */, hwnd, (HMENU) GetWindowLongPtrW(hwnd, GWLP_ID), ((CREATESTRUCT*)lParam)->hInstance, NULL); SetWindowLongPtr(hwnd, 0, (LONG_PTR)hwndCombo); } if (uMsg == WM_ENABLE && hwndChild) { EnableWindow(hwndChild, (BOOL)wParam); return 0; } if (uMsg == WM_SETTEXT && hwndChild) { SetWindowText(hwndChild, (TCHAR*)lParam); } if (uMsg == WM_GETTEXTLENGTH && hwndChild) { return (LRESULT)GetWindowTextLength(hwndChild); } if (uMsg == WM_GETTEXT && hwndChild) { return GetWindowText(hwndChild, (TCHAR*)lParam, (int)wParam); } if (uMsg == WM_SETFONT && hwndChild) { SendMessage(hwndChild, WM_SETFONT, wParam, lParam); int h = ALF__ComboItemHeight(hwnd); if (h) SendMessage(hwndChild, CB_SETITEMHEIGHT, (WPARAM)0, h); return 0; } if (uMsg == WM_GETFONT && hwndChild) { return SendMessage(hwndChild, WM_GETFONT, wParam, lParam); } if (uMsg == ALF_CB_GETSTYLE && hwndChild) { return GetWindowLong(hwndChild, GWL_STYLE); } if ((uMsg == ALF_CB_FINDSTRINGEXACT || uMsg == CB_FINDSTRINGEXACT) && hwndChild) { return SendMessage(hwndChild, CB_FINDSTRINGEXACT, wParam, lParam); } if ((uMsg == ALF_CB_ADDSTRING || uMsg == CB_ADDSTRING) && hwndChild) { return SendMessage(hwndChild, CB_ADDSTRING, wParam, lParam); } if ((uMsg == ALF_CB_INSERTSTRING || uMsg == CB_INSERTSTRING) && hwndChild) { return SendMessage(hwndChild, CB_INSERTSTRING, wParam, lParam); } if ((uMsg == ALF_CB_DELETESTRING || uMsg == CB_DELETESTRING) && hwndChild) { return SendMessage(hwndChild, CB_DELETESTRING, wParam, lParam); } if ((uMsg == ALF_CB_GETCURSEL || uMsg == CB_GETCURSEL) && hwndChild) { return SendMessage(hwndChild, CB_GETCURSEL, wParam, lParam); } if ((uMsg == ALF_CB_SETCURSEL || uMsg == CB_SETCURSEL) && hwndChild) { LRESULT curSel = SendMessage(hwndChild, CB_GETCURSEL, wParam, lParam); if ((WPARAM)curSel != wParam) { LRESULT newSel = SendMessage(hwndChild, CB_SETCURSEL, wParam, lParam); SendMessage(hwnd, WM_COMMAND, MAKEWPARAM(GetWindowLong(hwndChild, GWL_ID), CBN_SELCHANGE), (LPARAM)hwndChild); return newSel; } } if ((uMsg == ALF_CB_GETLBTEXTLEN || uMsg == CB_GETLBTEXTLEN) && hwndChild) { return SendMessage(hwndChild, CB_GETLBTEXTLEN, wParam, lParam); } if ((uMsg == ALF_CB_GETLBTEXT || uMsg == CB_GETLBTEXT) && hwndChild) { return SendMessage(hwndChild, CB_GETLBTEXT, wParam, lParam); } if (uMsg == WM_COMMAND && (HWND)lParam == hwndChild) { // XXX: for whatever reason, Win95 (and only win95 - doesn't happen on NT) // sends a wrong ID value in WPARAM. We fix it by replacing it with our own id. return SendMessage(GetParent(hwnd), WM_COMMAND, MAKEWPARAM(GetWindowLong(hwnd, GWL_ID), HIWORD(wParam)), (LPARAM)hwnd); } if (uMsg == ALF_WM_QUERYSIZE) { HDC hDc = GetDC(hwnd); HFONT font = (HFONT)SendMessage(hwnd, WM_GETFONT, 0, 0); if (font) { HFONT oldfont = SelectFont(hDc, font); TEXTMETRIC tm; ZeroMemory(&tm, sizeof(tm)); if (GetTextMetrics(hDc, &tm)) { SIZE *ps = (SIZE*)lParam; if (!ps->cx) { ps->cx = ALF_CentipointsToPixels(GetParent(hwnd), 12000); } if (!ps->cy) { ps->cy = tm.tmHeight + 2*ALF_Compat_GetSystemMetricsForDpi( SM_CYEDGE, ALF_CentipointsToPixels(GetParent(hwnd), 7200)) + 4 /* padding internal to the edit control */; } } SelectFont(hDc, oldfont); } ReleaseDC(hwnd, hDc); return 0; } if (uMsg == WM_MEASUREITEM) { PMEASUREITEMSTRUCT lpmis = (LPMEASUREITEMSTRUCT) lParam; int h = ALF__ComboItemHeight(hwnd); if (h) lpmis->itemHeight = h; return TRUE; } if (uMsg == WM_DRAWITEM) { LPDRAWITEMSTRUCT lpdis = (LPDRAWITEMSTRUCT) lParam; if (lpdis->itemID != (UINT)-1) { // Empty item DWORD clrForeground = SetTextColor(lpdis->hDC, GetSysColor(lpdis->itemState & (ODS_SELECTED | ODS_FOCUS) ? COLOR_HIGHLIGHTTEXT : COLOR_WINDOWTEXT)); DWORD clrBackground = SetBkColor(lpdis->hDC, GetSysColor(lpdis->itemState & (ODS_SELECTED | ODS_FOCUS) ? COLOR_HIGHLIGHT : COLOR_WINDOW)); LONG y = lpdis->rcItem.top; LONG x = lpdis->rcItem.left; if (LOBYTE(LOWORD(GetVersion())) < 4) { // baseline alignment for NT3.x/Win32s // ideally, this would only be applied for the ODS_COMOBOXEDIT state, // but that state is not sent pre-Win95. y += 1; } // add left padding like an edit control TEXTMETRIC tm; ZeroMemory(&tm, sizeof(tm)); if (GetTextMetrics(lpdis->hDC, &tm)) { if (tm.tmPitchAndFamily & (TMPF_VECTOR | TMPF_TRUETYPE)) x += ALF_GetAveCharWidth(lpdis->hDC) / 2; } // Get and display the text for the list item. int len = (int)SendMessage(lpdis->hwndItem, CB_GETLBTEXTLEN, (WPARAM)lpdis->itemID, 0); if (len != CB_ERR) { TCHAR *buf = (TCHAR*)LocalAlloc(LPTR, (len+1) * sizeof(TCHAR)); SendMessage(lpdis->hwndItem, CB_GETLBTEXT, lpdis->itemID, (LPARAM)buf); ExtTextOut(lpdis->hDC, x, y, ETO_CLIPPED | ETO_OPAQUE, &lpdis->rcItem, buf, lstrlen(buf), NULL); LocalFree(buf); } // Restore the previous colors. SetTextColor(lpdis->hDC, clrForeground); SetBkColor(lpdis->hDC, clrBackground); } // If the item has the focus, draw the focus rectangle. if ((lpdis->itemState & ODS_FOCUS) && !(lpdis->itemState & ODS_NOFOCUSRECT)) DrawFocusRect(lpdis->hDC, &lpdis->rcItem); return TRUE; } if (uMsg == WM_WINDOWPOSCHANGED && hwndChild) { WINDOWPOS *pos = (WINDOWPOS *)lParam; if (!(pos->flags & SWP_NOSIZE)) { // XXX: When resizing the combo box, it will improperly draw a selection. // this appears to be a well-known bug that is still not fixed, even in Win10. // workaround based on https://stackoverflow.com/questions/49603893/how-to-deal-with-invalidly-painted-combobox-control-in-win32-winapi DWORD sel = SendMessage(hwndChild, CB_GETEDITSEL, 0, 0); SendMessage(hwndChild, CB_SETEDITSEL, 0, -1); // SWP_NOCOPYBITS because NT 3.51 doesn't properly repaint the drop-down button SetWindowPos(hwndChild, NULL, 0, 0, pos->cx, 200, SWP_NOZORDER|SWP_NOACTIVATE|SWP_NOCOPYBITS); SendMessage(hwndChild, CB_SETEDITSEL, 0, (LPARAM)sel); DWORD heightOffset = 0; if (LOBYTE(LOWORD(GetVersion())) >= 4) { heightOffset = - 2*ALF_Compat_GetSystemMetricsForDpi( SM_CYEDGE, ALF_CentipointsToPixels(GetParent(hwnd), 7200)) - 2; } SendMessage(hwndChild, CB_SETITEMHEIGHT, (WPARAM)-1, pos->cy + heightOffset); int h = ALF__ComboItemHeight(hwnd); if (h) SendMessage(hwndChild, CB_SETITEMHEIGHT, (WPARAM)0, h); } } return DefWindowProc(hwnd, uMsg, wParam, lParam); } void ALF_RegisterComboClass(void) { WNDCLASS cls; ZeroMemory(&cls, sizeof(cls)); TCHAR classNameBuf[256]; ALF_BuildRandomClassName(TEXT("ALFComboBox"), classNameBuf, 256); cls.hInstance = ALF_HINSTANCE; cls.hCursor = LoadCursor(NULL, (LPTSTR)IDC_ARROW); if (LOBYTE(LOWORD(GetVersion())) >= 4) { cls.hbrBackground = (HBRUSH)(COLOR_BTNFACE+1); } else { // NT 3.x has white dialog backgrounds cls.hbrBackground = (HBRUSH)(COLOR_WINDOW+1); } cls.lpszClassName = classNameBuf; cls.cbWndExtra = sizeof(void*); cls.lpfnWndProc = ALF__ComboWindowProc; ATOM classatom = RegisterClass(&cls); if (!classatom) MessageBox(NULL, TEXT("FATAL: Could not register Combo class"), NULL, MB_OK); _alf_comboClass = MAKEINTATOM(classatom); } static HWND ALF_InternalAddComboBox(HWND win, WORD id, UINT x, UINT y, DWORD style, const TCHAR *defaultText) { ALFComboCreateParams cp; cp.comboStyle = style; HWND hwndCombo = CreateWindowEx(WS_EX_CONTROLPARENT, _alf_comboClass, TEXT(""), WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN, 0, 0, 0, 0, win, (HMENU)(int)id, (HINSTANCE)GetWindowLongPtr(win, GWLP_HINSTANCE), &cp); if (defaultText) SetWindowText(hwndCombo, defaultText); ALFWidgetLayoutParams p; ZeroMemory(&p, sizeof(p)); p.hwnd = hwndCombo; p.x = x; p.y = y; p.width = 0; p.height = 0; p.flags = ALF_QUERYSIZE | ALF_MESSAGEFONT; ALF_AddWidgetEx(win, &p); return hwndCombo; } HWND ALF_AddEditableComboBox(HWND win, WORD id, UINT x, UINT y, const TCHAR *defaultText) { return ALF_InternalAddComboBox(win, id, x, y, CBS_DROPDOWN, defaultText); } HWND ALF_AddSelectionOnlyComboBox(HWND win, WORD id, UINT x, UINT y) { return ALF_InternalAddComboBox(win, id, x, y, CBS_DROPDOWNLIST, NULL); } int /* index */ ALF_ComboBoxAddString(HWND combo, const TCHAR *text) { return (int)SendMessage(combo, ALF_CB_ADDSTRING, 0, (LPARAM)text); } TCHAR * ALF_ComboBoxCurrentText(HWND combo) { LRESULT style = SendMessage(combo, ALF_CB_GETSTYLE, 0, 0); if (style & CBS_DROPDOWNLIST) { int i = ALF_ComboBoxCurrentIndex(combo); if (i >= 0) { return ALF_ComboBoxStringForIndex(combo, i); } } return ALF_Text(combo); } void ALF_ComboBoxSetText(HWND combo, const TCHAR *text) { LRESULT index = SendMessage(combo, ALF_CB_FINDSTRINGEXACT, (WPARAM)-1, (LPARAM)text); if (index >= 0) { SendMessage(combo, ALF_CB_SETCURSEL, (WPARAM)index, 0); } else { SetWindowText(combo, text); } } int ALF_ComboBoxCurrentIndex(HWND combo) { return (int)SendMessage(combo, ALF_CB_GETCURSEL, 0, 0); } void ALF_ComboBoxSetCurrentIndex(HWND combo, int index) { SendMessage(combo, ALF_CB_SETCURSEL, (WPARAM)index, 0); } TCHAR * // free with ALF_Free ALF_ComboBoxStringForIndex(HWND combo, int index) { int len = (int)SendMessage(combo, ALF_CB_GETLBTEXTLEN, (WPARAM)index, 0); if (len > 0) { TCHAR* buf = ALF_New(TCHAR, len + 1); if (SendMessage(combo, ALF_CB_GETLBTEXT, (WPARAM)index, (LPARAM)buf)) { return buf; } else { ALF_Free(buf); } } return NULL; } void ALF_ComboBoxInsertString(HWND combo, int index, const TCHAR *text) { SendMessage(combo, ALF_CB_INSERTSTRING, (WPARAM)index, (LPARAM)text); } void ALF_ComboBoxRemoveString(HWND combo, int index) { SendMessage(combo, ALF_CB_DELETESTRING, (WPARAM)index, 0); }