// 11 february 2014 package ui import ( "fmt" "sync" "syscall" "unsafe" ) type sysData struct { cSysData hwnd _HWND children map[_HMENU]*sysData nextChildID _HMENU childrenLock sync.Mutex isMarquee bool // for sysData.setProgress() // unlike with GTK+ and Mac OS X, we're responsible for sizing Area properly ourselves areawidth int areaheight int clickCounter clickCounter lastfocus _HWND } type classData struct { name *uint16 style uint32 xstyle uint32 altStyle uint32 storeSysData bool doNotLoadFont bool appendMsg uintptr insertBeforeMsg uintptr deleteMsg uintptr selectedIndexMsg uintptr selectedIndexErr uintptr addSpaceErr uintptr lenMsg uintptr } const controlstyle = _WS_CHILD | _WS_VISIBLE | _WS_TABSTOP const controlxstyle = 0 var classTypes = [nctypes]*classData{ c_window: &classData{ name: stdWndClass, style: _WS_OVERLAPPEDWINDOW, xstyle: 0, storeSysData: true, doNotLoadFont: true, }, c_button: &classData{ name: toUTF16("BUTTON"), style: _BS_PUSHBUTTON | controlstyle, xstyle: 0 | controlxstyle, }, c_checkbox: &classData{ name: toUTF16("BUTTON"), // don't use BS_AUTOCHECKBOX because http://blogs.msdn.com/b/oldnewthing/archive/2014/05/22/10527522.aspx style: _BS_CHECKBOX | controlstyle, xstyle: 0 | controlxstyle, }, c_combobox: &classData{ name: toUTF16("COMBOBOX"), style: _CBS_DROPDOWNLIST | _WS_VSCROLL | controlstyle, xstyle: 0 | controlxstyle, altStyle: _CBS_DROPDOWN | _CBS_AUTOHSCROLL | _WS_VSCROLL | controlstyle, appendMsg: _CB_ADDSTRING, insertBeforeMsg: _CB_INSERTSTRING, deleteMsg: _CB_DELETESTRING, selectedIndexMsg: _CB_GETCURSEL, selectedIndexErr: negConst(_CB_ERR), addSpaceErr: negConst(_CB_ERRSPACE), lenMsg: _CB_GETCOUNT, }, c_lineedit: &classData{ name: toUTF16("EDIT"), // WS_EX_CLIENTEDGE without WS_BORDER will apply visual styles // thanks to MindChild in irc.efnet.net/#winprog style: _ES_AUTOHSCROLL | controlstyle, xstyle: _WS_EX_CLIENTEDGE | controlxstyle, altStyle: _ES_PASSWORD | _ES_AUTOHSCROLL | controlstyle, }, c_label: &classData{ name: toUTF16("STATIC"), // SS_NOPREFIX avoids accelerator translation; SS_LEFTNOWORDWRAP clips text past the end // controls are vertically aligned to the top by default (thanks Xeek in irc.freenode.net/#winapi) // also note that tab stops are remove dfor labels style: (_SS_NOPREFIX | _SS_LEFTNOWORDWRAP | controlstyle) &^ _WS_TABSTOP, xstyle: 0 | controlxstyle, // MAKE SURE THIS IS THE SAME altStyle: (_SS_NOPREFIX | _SS_LEFTNOWORDWRAP | controlstyle) &^ _WS_TABSTOP, }, c_listbox: &classData{ name: toUTF16("LISTBOX"), // we don't use LBS_STANDARD because it sorts (and has WS_BORDER; see above) // LBS_NOINTEGRALHEIGHT gives us exactly the size we want // LBS_MULTISEL sounds like it does what we want but it actually doesn't; instead, it toggles item selection regardless of modifier state, which doesn't work like anything else (see http://msdn.microsoft.com/en-us/library/windows/desktop/bb775149%28v=vs.85%29.aspx and http://msdn.microsoft.com/en-us/library/windows/desktop/aa511485.aspx) style: _LBS_NOTIFY | _LBS_NOINTEGRALHEIGHT | _WS_VSCROLL | controlstyle, xstyle: _WS_EX_CLIENTEDGE | controlxstyle, altStyle: _LBS_EXTENDEDSEL | _LBS_NOTIFY | _LBS_NOINTEGRALHEIGHT | _WS_VSCROLL | controlstyle, appendMsg: _LB_ADDSTRING, insertBeforeMsg: _LB_INSERTSTRING, deleteMsg: _LB_DELETESTRING, selectedIndexMsg: _LB_GETCURSEL, selectedIndexErr: negConst(_LB_ERR), addSpaceErr: negConst(_LB_ERRSPACE), lenMsg: _LB_GETCOUNT, }, c_progressbar: &classData{ name: toUTF16(x_PROGRESS_CLASS), // note that tab stops are disabled for progress bars style: (_PBS_SMOOTH | controlstyle) &^ _WS_TABSTOP, xstyle: 0 | controlxstyle, doNotLoadFont: true, }, c_area: &classData{ name: areaWndClass, style: areastyle, xstyle: areaxstyle, storeSysData: true, doNotLoadFont: true, }, } func (s *sysData) addChild(child *sysData) _HMENU { s.childrenLock.Lock() defer s.childrenLock.Unlock() s.nextChildID++ // start at 1 if s.children == nil { s.children = map[_HMENU]*sysData{} } s.children[s.nextChildID] = child return s.nextChildID } func (s *sysData) delChild(id _HMENU) { s.childrenLock.Lock() defer s.childrenLock.Unlock() delete(s.children, id) } var ( _blankString = toUTF16("") blankString = utf16ToArg(_blankString) ) func (s *sysData) make(window *sysData) (err error) { ct := classTypes[s.ctype] cid := _HMENU(0) pwin := uintptr(_NULL) if window != nil { // this is a child control cid = window.addChild(s) pwin = uintptr(window.hwnd) } style := uintptr(ct.style) if s.alternate { style = uintptr(ct.altStyle) } lpParam := uintptr(_NULL) if ct.storeSysData { lpParam = uintptr(unsafe.Pointer(s)) } r1, _, err := _createWindowEx.Call( uintptr(ct.xstyle), utf16ToArg(ct.name), blankString, // we set the window text later style, negConst(_CW_USEDEFAULT), negConst(_CW_USEDEFAULT), negConst(_CW_USEDEFAULT), negConst(_CW_USEDEFAULT), pwin, uintptr(cid), uintptr(hInstance), lpParam) if r1 == 0 { // failure if window != nil { window.delChild(cid) } panic(fmt.Errorf("error actually creating window/control: %v", err)) } if !ct.storeSysData { // regular control; store s.hwnd ourselves s.hwnd = _HWND(r1) } else if s.hwnd != _HWND(r1) { // we store sysData in storeSysData(); sanity check panic(fmt.Errorf("hwnd mismatch creating window/control: storeSysData() stored 0x%X but CreateWindowEx() returned 0x%X", s.hwnd, r1)) } if !ct.doNotLoadFont { _sendMessage.Call( uintptr(s.hwnd), uintptr(_WM_SETFONT), uintptr(_WPARAM(controlFont)), uintptr(_LPARAM(_TRUE))) } return nil } var ( _updateWindow = user32.NewProc("UpdateWindow") ) // if the object is a window, we need to do the following the first time // ShowWindow(hwnd, nCmdShow); // UpdateWindow(hwnd); func (s *sysData) firstShow() error { _showWindow.Call( uintptr(s.hwnd), uintptr(nCmdShow)) r1, _, err := _updateWindow.Call(uintptr(s.hwnd)) if r1 == 0 { // failure panic(fmt.Errorf("error updating window for the first time: %v", err)) } return nil } func (s *sysData) show() { _showWindow.Call( uintptr(s.hwnd), uintptr(_SW_SHOW)) } func (s *sysData) hide() { _showWindow.Call( uintptr(s.hwnd), uintptr(_SW_HIDE)) } func (s *sysData) setText(text string) { ptext := toUTF16(text) r1, _, err := _setWindowText.Call( uintptr(s.hwnd), utf16ToArg(ptext)) if r1 == 0 { // failure panic(fmt.Errorf("error setting window/control text: %v", err)) } } func (s *sysData) setRect(x int, y int, width int, height int, winheight int) error { r1, _, err := _moveWindow.Call( uintptr(s.hwnd), uintptr(x), uintptr(y), uintptr(width), uintptr(height), uintptr(_TRUE)) if r1 == 0 { // failure return fmt.Errorf("error setting window/control rect: %v", err) } return nil } func (s *sysData) isChecked() bool { r1, _, _ := _sendMessage.Call( uintptr(s.hwnd), uintptr(_BM_GETCHECK), uintptr(0), uintptr(0)) return r1 == _BST_CHECKED } func (s *sysData) text() (str string) { var tc []uint16 r1, _, _ := _sendMessage.Call( uintptr(s.hwnd), uintptr(_WM_GETTEXTLENGTH), uintptr(0), uintptr(0)) length := r1 + 1 // terminating null tc = make([]uint16, length) _sendMessage.Call( uintptr(s.hwnd), uintptr(_WM_GETTEXT), uintptr(_WPARAM(length)), uintptr(_LPARAM(unsafe.Pointer(&tc[0])))) return syscall.UTF16ToString(tc) } func (s *sysData) append(what string) { pwhat := toUTF16(what) r1, _, err := _sendMessage.Call( uintptr(s.hwnd), uintptr(classTypes[s.ctype].appendMsg), uintptr(_WPARAM(0)), utf16ToLPARAM(pwhat)) if r1 == uintptr(classTypes[s.ctype].addSpaceErr) { panic(fmt.Errorf("out of space adding item to combobox/listbox (last error: %v)", err)) } else if r1 == uintptr(classTypes[s.ctype].selectedIndexErr) { panic(fmt.Errorf("failed to add item to combobox/listbox (last error: %v)", err)) } } func (s *sysData) insertBefore(what string, index int) { pwhat := toUTF16(what) r1, _, err := _sendMessage.Call( uintptr(s.hwnd), uintptr(classTypes[s.ctype].insertBeforeMsg), uintptr(_WPARAM(index)), utf16ToLPARAM(pwhat)) if r1 == uintptr(classTypes[s.ctype].addSpaceErr) { panic(fmt.Errorf("out of space adding item to combobox/listbox (last error: %v)", err)) } else if r1 == uintptr(classTypes[s.ctype].selectedIndexErr) { panic(fmt.Errorf("failed to add item to combobox/listbox (last error: %v)", err)) } } func (s *sysData) selectedIndex() int { r1, _, _ := _sendMessage.Call( uintptr(s.hwnd), uintptr(classTypes[s.ctype].selectedIndexMsg), uintptr(_WPARAM(0)), uintptr(_LPARAM(0))) if r1 == uintptr(classTypes[s.ctype].selectedIndexErr) { // no selection or manually entered text (apparently, for the latter) return -1 } return int(r1) } func (s *sysData) selectedIndices() []int { if !s.alternate { // single-selection list box; use single-selection method index := s.selectedIndex() if index == -1 { return nil } return []int{index} } r1, _, err := _sendMessage.Call( uintptr(s.hwnd), uintptr(_LB_GETSELCOUNT), uintptr(0), uintptr(0)) if r1 == negConst(_LB_ERR) { panic(fmt.Errorf("error: LB_ERR from LB_GETSELCOUNT in what we know is a multi-selection listbox: %v", err)) } if r1 == 0 { // nothing selected return nil } indices := make([]int, r1) r1, _, err = _sendMessage.Call( uintptr(s.hwnd), uintptr(_LB_GETSELITEMS), uintptr(_WPARAM(r1)), uintptr(_LPARAM(unsafe.Pointer(&indices[0])))) if r1 == negConst(_LB_ERR) { panic(fmt.Errorf("error: LB_ERR from LB_GETSELITEMS in what we know is a multi-selection listbox: %v", err)) } return indices } func (s *sysData) selectedTexts() []string { indices := s.selectedIndices() strings := make([]string, len(indices)) for i, v := range indices { r1, _, err := _sendMessage.Call( uintptr(s.hwnd), uintptr(_LB_GETTEXTLEN), uintptr(_WPARAM(v)), uintptr(0)) if r1 == negConst(_LB_ERR) { panic(fmt.Errorf("error: LB_ERR from LB_GETTEXTLEN in what we know is a valid listbox index (came from LB_GETSELITEMS): %v", err)) } str := make([]uint16, r1) r1, _, err = _sendMessage.Call( uintptr(s.hwnd), uintptr(_LB_GETTEXT), uintptr(_WPARAM(v)), uintptr(_LPARAM(unsafe.Pointer(&str[0])))) if r1 == negConst(_LB_ERR) { panic(fmt.Errorf("error: LB_ERR from LB_GETTEXT in what we know is a valid listbox index (came from LB_GETSELITEMS): %v", err)) } strings[i] = syscall.UTF16ToString(str) } return strings } func (s *sysData) setWindowSize(width int, height int) error { var rect _RECT r1, _, err := _getClientRect.Call( uintptr(s.hwnd), uintptr(unsafe.Pointer(&rect))) if r1 == 0 { panic(fmt.Errorf("error getting upper-left of window for resize: %v", err)) } // TODO AdjustWindowRect() on the result // 0 because (0,0) is top-left so no winheight err = s.setRect(int(rect.left), int(rect.top), width, height, 0) if err != nil { panic(fmt.Errorf("error actually resizing window: %v", err)) } return nil } func (s *sysData) delete(index int) { r1, _, err := _sendMessage.Call( uintptr(s.hwnd), uintptr(classTypes[s.ctype].deleteMsg), uintptr(_WPARAM(index)), uintptr(0)) if r1 == uintptr(classTypes[s.ctype].selectedIndexErr) { panic(fmt.Errorf("failed to delete item from combobox/listbox (last error: %v)", err)) } } func (s *sysData) setIndeterminate() { r1, _, err := _setWindowLongPtr.Call( uintptr(s.hwnd), negConst(_GWL_STYLE), uintptr(classTypes[s.ctype].style | _PBS_MARQUEE)) if r1 == 0 { panic(fmt.Errorf("error setting progress bar style to enter indeterminate mode: %v", err)) } _sendMessage.Call( uintptr(s.hwnd), uintptr(_PBM_SETMARQUEE), uintptr(_WPARAM(_TRUE)), uintptr(0)) s.isMarquee = true } func (s *sysData) setProgress(percent int) { if percent == -1 { s.setIndeterminate() return } if s.isMarquee { // turn off marquee before switching back _sendMessage.Call( uintptr(s.hwnd), uintptr(_PBM_SETMARQUEE), uintptr(_WPARAM(_FALSE)), uintptr(0)) r1, _, err := _setWindowLongPtr.Call( uintptr(s.hwnd), negConst(_GWL_STYLE), uintptr(classTypes[s.ctype].style)) if r1 == 0 { panic(fmt.Errorf("error setting progress bar style to leave indeterminate mode (percent %d): %v", percent, err)) } s.isMarquee = false } send := func(msg uintptr, n int, l _LPARAM) { _sendMessage.Call( uintptr(s.hwnd), msg, uintptr(_WPARAM(n)), uintptr(l)) } // Windows 7 has a non-disableable slowly-animating progress bar increment // there isn't one for decrement, so we'll work around by going one higher and then lower again // for the case where percent == 100, we need to increase the range temporarily // sources: http://social.msdn.microsoft.com/Forums/en-US/61350dc7-6584-4c4e-91b0-69d642c03dae/progressbar-disable-smooth-animation http://stackoverflow.com/questions/2217688/windows-7-aero-theme-progress-bar-bug http://discuss.joelonsoftware.com/default.asp?dotnet.12.600456.2 http://stackoverflow.com/questions/22469876/progressbar-lag-when-setting-position-with-pbm-setpos http://stackoverflow.com/questions/6128287/tprogressbar-never-fills-up-all-the-way-seems-to-be-updating-too-fast if percent == 100 { send(_PBM_SETRANGE32, 0, 101) } send(_PBM_SETPOS, percent+1, 0) send(_PBM_SETPOS, percent, 0) if percent == 100 { send(_PBM_SETRANGE32, 0, 100) } } func (s *sysData) len() int { r1, _, err := _sendMessage.Call( uintptr(s.hwnd), uintptr(classTypes[s.ctype].lenMsg), uintptr(_WPARAM(0)), uintptr(_LPARAM(0))) if r1 == uintptr(classTypes[s.ctype].selectedIndexErr) { panic(fmt.Errorf("unexpected error return from sysData.len(); GetLastError() says %v", err)) } return int(r1) } func (s *sysData) setAreaSize(width int, height int) { _sendMessage.Call( uintptr(s.hwnd), uintptr(msgSetAreaSize), uintptr(width), // WPARAM is UINT_PTR on Windows XP and newer at least, so we're good with this uintptr(height)) } func (s *sysData) repaintAll() { _sendMessage.Call( uintptr(s.hwnd), uintptr(msgRepaintAll), uintptr(0), uintptr(0)) } func (s *sysData) center() { var ws _RECT r1, _, err := _getWindowRect.Call( uintptr(s.hwnd), uintptr(unsafe.Pointer(&ws))) if r1 == 0 { panic(fmt.Errorf("error getting window rect for sysData.center(): %v", err)) } // TODO should this be using the monitor functions instead? http://blogs.msdn.com/b/oldnewthing/archive/2005/05/05/414910.aspx // error returns from GetSystemMetrics() is meaningless because the return value, 0, is still valid // TODO should this be using the client rect and not the window rect? dw, _, _ := _getSystemMetrics.Call(uintptr(_SM_CXFULLSCREEN)) dh, _, _ := _getSystemMetrics.Call(uintptr(_SM_CYFULLSCREEN)) ww := ws.right - ws.left wh := ws.bottom - ws.top wx := (int32(dw) / 2) - (ww / 2) wy := (int32(dh) / 2) - (wh / 2) s.setRect(int(wx), int(wy), int(ww), int(wh), 0) } func (s *sysData) setChecked(checked bool) { c := uintptr(_BST_CHECKED) if !checked { c = uintptr(_BST_UNCHECKED) } _sendMessage.Call( uintptr(s.hwnd), uintptr(_BM_SETCHECK), c, uintptr(0)) }