# DockingBar.py # Ported directly (comments and all) from the samples at www.codeguru.com # WARNING: Use at your own risk, as this interface is highly likely to change. # Currently we support only one child per DockingBar. Later we need to add # support for multiple children. import struct import win32api import win32con import win32ui from pywin.mfc import afxres, window clrBtnHilight = win32api.GetSysColor(win32con.COLOR_BTNHILIGHT) clrBtnShadow = win32api.GetSysColor(win32con.COLOR_BTNSHADOW) def CenterPoint(rect): width = rect[2] - rect[0] height = rect[3] - rect[1] return rect[0] + width // 2, rect[1] + height // 2 def OffsetRect(rect, point): (x, y) = point return rect[0] + x, rect[1] + y, rect[2] + x, rect[3] + y def DeflateRect(rect, point): (x, y) = point return rect[0] + x, rect[1] + y, rect[2] - x, rect[3] - y def PtInRect(rect, pt): return rect[0] <= pt[0] < rect[2] and rect[1] <= pt[1] < rect[3] class DockingBar(window.Wnd): def __init__(self, obj=None): if obj is None: obj = win32ui.CreateControlBar() window.Wnd.__init__(self, obj) self.dialog = None self.nDockBarID = 0 self.sizeMin = 32, 32 self.sizeHorz = 200, 200 self.sizeVert = 200, 200 self.sizeFloat = 200, 200 self.bTracking = 0 self.bInRecalcNC = 0 self.cxEdge = 6 self.cxBorder = 3 self.cxGripper = 20 self.brushBkgd = win32ui.CreateBrush() self.brushBkgd.CreateSolidBrush(win32api.GetSysColor(win32con.COLOR_BTNFACE)) # Support for diagonal resizing self.cyBorder = 3 self.cCaptionSize = win32api.GetSystemMetrics(win32con.SM_CYSMCAPTION) self.cMinWidth = win32api.GetSystemMetrics(win32con.SM_CXMIN) self.cMinHeight = win32api.GetSystemMetrics(win32con.SM_CYMIN) self.rectUndock = (0, 0, 0, 0) def OnUpdateCmdUI(self, target, bDisableIfNoHndler): return self.UpdateDialogControls(target, bDisableIfNoHndler) def CreateWindow( self, parent, childCreator, title, id, style=win32con.WS_CHILD | win32con.WS_VISIBLE | afxres.CBRS_LEFT, childCreatorArgs=(), ): assert not ( (style & afxres.CBRS_SIZE_FIXED) and (style & afxres.CBRS_SIZE_DYNAMIC) ), "Invalid style" self.rectClose = self.rectBorder = self.rectGripper = self.rectTracker = ( 0, 0, 0, 0, ) # save the style self._obj_.dwStyle = style & afxres.CBRS_ALL cursor = win32api.LoadCursor(0, win32con.IDC_ARROW) wndClass = win32ui.RegisterWndClass( win32con.CS_DBLCLKS, cursor, self.brushBkgd.GetSafeHandle(), 0 ) self._obj_.CreateWindow(wndClass, title, style, (0, 0, 0, 0), parent, id) # Create the child dialog self.dialog = childCreator(*(self,) + childCreatorArgs) # use the dialog dimensions as default base dimensions assert self.dialog.IsWindow(), ( "The childCreator function %s did not create a window!" % childCreator ) rect = self.dialog.GetWindowRect() self.sizeHorz = self.sizeVert = self.sizeFloat = ( rect[2] - rect[0], rect[3] - rect[1], ) self.sizeHorz = self.sizeHorz[0], self.sizeHorz[1] + self.cxEdge + self.cxBorder self.sizeVert = self.sizeVert[0] + self.cxEdge + self.cxBorder, self.sizeVert[1] self.HookMessages() def CalcFixedLayout(self, bStretch, bHorz): rectTop = self.dockSite.GetControlBar( afxres.AFX_IDW_DOCKBAR_TOP ).GetWindowRect() rectLeft = self.dockSite.GetControlBar( afxres.AFX_IDW_DOCKBAR_LEFT ).GetWindowRect() if bStretch: nHorzDockBarWidth = 32767 nVertDockBarHeight = 32767 else: nHorzDockBarWidth = rectTop[2] - rectTop[0] + 4 nVertDockBarHeight = rectLeft[3] - rectLeft[1] + 4 if self.IsFloating(): return self.sizeFloat if bHorz: return nHorzDockBarWidth, self.sizeHorz[1] return self.sizeVert[0], nVertDockBarHeight def CalcDynamicLayout(self, length, mode): # Support for diagonal sizing. if self.IsFloating(): self.GetParent().GetParent().ModifyStyle(win32ui.MFS_4THICKFRAME, 0) if mode & (win32ui.LM_HORZDOCK | win32ui.LM_VERTDOCK): flags = ( win32con.SWP_NOSIZE | win32con.SWP_NOMOVE | win32con.SWP_NOZORDER | win32con.SWP_NOACTIVATE | win32con.SWP_FRAMECHANGED ) self.SetWindowPos( 0, ( 0, 0, 0, 0, ), flags, ) self.dockSite.RecalcLayout() return self._obj_.CalcDynamicLayout(length, mode) if mode & win32ui.LM_MRUWIDTH: return self.sizeFloat if mode & win32ui.LM_COMMIT: self.sizeFloat = length, self.sizeFloat[1] return self.sizeFloat # More diagonal sizing. if self.IsFloating(): dc = self.dockContext pt = win32api.GetCursorPos() windowRect = self.GetParent().GetParent().GetWindowRect() hittest = dc.nHitTest if hittest == win32con.HTTOPLEFT: cx = max(windowRect[2] - pt[0], self.cMinWidth) - self.cxBorder cy = max(windowRect[3] - self.cCaptionSize - pt[1], self.cMinHeight) - 1 self.sizeFloat = cx, cy top = ( min(pt[1], windowRect[3] - self.cCaptionSize - self.cMinHeight) - self.cyBorder ) left = min(pt[0], windowRect[2] - self.cMinWidth) - 1 dc.rectFrameDragHorz = ( left, top, dc.rectFrameDragHorz[2], dc.rectFrameDragHorz[3], ) return self.sizeFloat if hittest == win32con.HTTOPRIGHT: cx = max(pt[0] - windowRect[0], self.cMinWidth) cy = max(windowRect[3] - self.cCaptionSize - pt[1], self.cMinHeight) - 1 self.sizeFloat = cx, cy top = ( min(pt[1], windowRect[3] - self.cCaptionSize - self.cMinHeight) - self.cyBorder ) dc.rectFrameDragHorz = ( dc.rectFrameDragHorz[0], top, dc.rectFrameDragHorz[2], dc.rectFrameDragHorz[3], ) return self.sizeFloat if hittest == win32con.HTBOTTOMLEFT: cx = max(windowRect[2] - pt[0], self.cMinWidth) - self.cxBorder cy = max(pt[1] - windowRect[1] - self.cCaptionSize, self.cMinHeight) self.sizeFloat = cx, cy left = min(pt[0], windowRect[2] - self.cMinWidth) - 1 dc.rectFrameDragHorz = ( left, dc.rectFrameDragHorz[1], dc.rectFrameDragHorz[2], dc.rectFrameDragHorz[3], ) return self.sizeFloat if hittest == win32con.HTBOTTOMRIGHT: cx = max(pt[0] - windowRect[0], self.cMinWidth) cy = max(pt[1] - windowRect[1] - self.cCaptionSize, self.cMinHeight) self.sizeFloat = cx, cy return self.sizeFloat if mode & win32ui.LM_LENGTHY: self.sizeFloat = self.sizeFloat[0], max(self.sizeMin[1], length) return self.sizeFloat else: return max(self.sizeMin[0], length), self.sizeFloat[1] def OnWindowPosChanged(self, msg): if self.GetSafeHwnd() == 0 or self.dialog is None: return 0 lparam = msg[3] """ LPARAM used with WM_WINDOWPOSCHANGED: typedef struct { HWND hwnd; HWND hwndInsertAfter; int x; int y; int cx; int cy; UINT flags;} WINDOWPOS; """ format = "PPiiiii" bytes = win32ui.GetBytes(lparam, struct.calcsize(format)) hwnd, hwndAfter, x, y, cx, cy, flags = struct.unpack(format, bytes) if self.bInRecalcNC: rc = self.GetClientRect() self.dialog.MoveWindow(rc) return 0 # Find on which side are we docked nDockBarID = self.GetParent().GetDlgCtrlID() # Return if dropped at same location # no docking side change and no size change if ( (nDockBarID == self.nDockBarID) and (flags & win32con.SWP_NOSIZE) and ( (self._obj_.dwStyle & afxres.CBRS_BORDER_ANY) != afxres.CBRS_BORDER_ANY ) ): return self.nDockBarID = nDockBarID # Force recalc the non-client area self.bInRecalcNC = 1 try: swpflags = ( win32con.SWP_NOSIZE | win32con.SWP_NOMOVE | win32con.SWP_NOZORDER | win32con.SWP_FRAMECHANGED ) self.SetWindowPos(0, (0, 0, 0, 0), swpflags) finally: self.bInRecalcNC = 0 return 0 # This is a virtual and not a message hook. def OnSetCursor(self, window, nHitTest, wMouseMsg): if nHitTest != win32con.HTSIZE or self.bTracking: return self._obj_.OnSetCursor(window, nHitTest, wMouseMsg) if self.IsHorz(): win32api.SetCursor(win32api.LoadCursor(0, win32con.IDC_SIZENS)) else: win32api.SetCursor(win32api.LoadCursor(0, win32con.IDC_SIZEWE)) return 1 # Mouse Handling def OnLButtonUp(self, msg): if not self.bTracking: return 1 # pass it on. self.StopTracking(1) return 0 # Dont pass on def OnLButtonDown(self, msg): # UINT nFlags, CPoint point) # only start dragging if clicked in "void" space if self.dockBar is not None: # start the drag pt = msg[5] pt = self.ClientToScreen(pt) self.dockContext.StartDrag(pt) return 0 return 1 def OnNcLButtonDown(self, msg): if self.bTracking: return 0 nHitTest = wparam = msg[2] pt = msg[5] if nHitTest == win32con.HTSYSMENU and not self.IsFloating(): self.GetDockingFrame().ShowControlBar(self, 0, 0) elif nHitTest == win32con.HTMINBUTTON and not self.IsFloating(): self.dockContext.ToggleDocking() elif ( nHitTest == win32con.HTCAPTION and not self.IsFloating() and self.dockBar is not None ): self.dockContext.StartDrag(pt) elif nHitTest == win32con.HTSIZE and not self.IsFloating(): self.StartTracking() else: return 1 return 0 def OnLButtonDblClk(self, msg): # only toggle docking if clicked in "void" space if self.dockBar is not None: # toggle docking self.dockContext.ToggleDocking() return 0 return 1 def OnNcLButtonDblClk(self, msg): nHitTest = wparam = msg[2] # UINT nHitTest, CPoint point) if self.dockBar is not None and nHitTest == win32con.HTCAPTION: # toggle docking self.dockContext.ToggleDocking() return 0 return 1 def OnMouseMove(self, msg): flags = wparam = msg[2] lparam = msg[3] if self.IsFloating() or not self.bTracking: return 1 # Convert unsigned 16 bit to signed 32 bit. x = win32api.LOWORD(lparam) if x & 32768: x = x | -65536 y = win32api.HIWORD(lparam) if y & 32768: y = y | -65536 pt = x, y cpt = CenterPoint(self.rectTracker) pt = self.ClientToWnd(pt) if self.IsHorz(): if cpt[1] != pt[1]: self.OnInvertTracker(self.rectTracker) self.rectTracker = OffsetRect(self.rectTracker, (0, pt[1] - cpt[1])) self.OnInvertTracker(self.rectTracker) else: if cpt[0] != pt[0]: self.OnInvertTracker(self.rectTracker) self.rectTracker = OffsetRect(self.rectTracker, (pt[0] - cpt[0], 0)) self.OnInvertTracker(self.rectTracker) return 0 # Dont pass it on. # def OnBarStyleChange(self, old, new): def OnNcCalcSize(self, bCalcValid, size_info): (rc0, rc1, rc2, pos) = size_info self.rectBorder = self.GetWindowRect() self.rectBorder = OffsetRect( self.rectBorder, (-self.rectBorder[0], -self.rectBorder[1]) ) dwBorderStyle = self._obj_.dwStyle | afxres.CBRS_BORDER_ANY if self.nDockBarID == afxres.AFX_IDW_DOCKBAR_TOP: dwBorderStyle = dwBorderStyle & ~afxres.CBRS_BORDER_BOTTOM rc0.left = rc0.left + self.cxGripper rc0.bottom = rc0.bottom - self.cxEdge rc0.top = rc0.top + self.cxBorder rc0.right = rc0.right - self.cxBorder self.rectBorder = ( self.rectBorder[0], self.rectBorder[3] - self.cxEdge, self.rectBorder[2], self.rectBorder[3], ) elif self.nDockBarID == afxres.AFX_IDW_DOCKBAR_BOTTOM: dwBorderStyle = dwBorderStyle & ~afxres.CBRS_BORDER_TOP rc0.left = rc0.left + self.cxGripper rc0.top = rc0.top + self.cxEdge rc0.bottom = rc0.bottom - self.cxBorder rc0.right = rc0.right - self.cxBorder self.rectBorder = ( self.rectBorder[0], self.rectBorder[1], self.rectBorder[2], self.rectBorder[1] + self.cxEdge, ) elif self.nDockBarID == afxres.AFX_IDW_DOCKBAR_LEFT: dwBorderStyle = dwBorderStyle & ~afxres.CBRS_BORDER_RIGHT rc0.right = rc0.right - self.cxEdge rc0.left = rc0.left + self.cxBorder rc0.bottom = rc0.bottom - self.cxBorder rc0.top = rc0.top + self.cxGripper self.rectBorder = ( self.rectBorder[2] - self.cxEdge, self.rectBorder[1], self.rectBorder[2], self.rectBorder[3], ) elif self.nDockBarID == afxres.AFX_IDW_DOCKBAR_RIGHT: dwBorderStyle = dwBorderStyle & ~afxres.CBRS_BORDER_LEFT rc0.left = rc0.left + self.cxEdge rc0.right = rc0.right - self.cxBorder rc0.bottom = rc0.bottom - self.cxBorder rc0.top = rc0.top + self.cxGripper self.rectBorder = ( self.rectBorder[0], self.rectBorder[1], self.rectBorder[0] + self.cxEdge, self.rectBorder[3], ) else: self.rectBorder = 0, 0, 0, 0 self.SetBarStyle(dwBorderStyle) return 0 def OnNcPaint(self, msg): self.EraseNonClient() dc = self.GetWindowDC() ctl = win32api.GetSysColor(win32con.COLOR_BTNHIGHLIGHT) cbr = win32api.GetSysColor(win32con.COLOR_BTNSHADOW) dc.Draw3dRect(self.rectBorder, ctl, cbr) self.DrawGripper(dc) rect = self.GetClientRect() self.InvalidateRect(rect, 1) return 0 def OnNcHitTest(self, pt): # A virtual, not a hooked message. if self.IsFloating(): return 1 ptOrig = pt rect = self.GetWindowRect() pt = pt[0] - rect[0], pt[1] - rect[1] if PtInRect(self.rectClose, pt): return win32con.HTSYSMENU elif PtInRect(self.rectUndock, pt): return win32con.HTMINBUTTON elif PtInRect(self.rectGripper, pt): return win32con.HTCAPTION elif PtInRect(self.rectBorder, pt): return win32con.HTSIZE else: return self._obj_.OnNcHitTest(ptOrig) def StartTracking(self): self.SetCapture() # make sure no updates are pending self.RedrawWindow(None, None, win32con.RDW_ALLCHILDREN | win32con.RDW_UPDATENOW) self.dockSite.LockWindowUpdate() self.ptOld = CenterPoint(self.rectBorder) self.bTracking = 1 self.rectTracker = self.rectBorder if not self.IsHorz(): l, t, r, b = self.rectTracker b = b - 4 self.rectTracker = l, t, r, b self.OnInvertTracker(self.rectTracker) def OnCaptureChanged(self, msg): hwnd = lparam = msg[3] if self.bTracking and hwnd != self.GetSafeHwnd(): self.StopTracking(0) # cancel tracking return 1 def StopTracking(self, bAccept): self.OnInvertTracker(self.rectTracker) self.dockSite.UnlockWindowUpdate() self.bTracking = 0 self.ReleaseCapture() if not bAccept: return rcc = self.dockSite.GetWindowRect() if self.IsHorz(): newsize = self.sizeHorz[1] maxsize = newsize + (rcc[3] - rcc[1]) minsize = self.sizeMin[1] else: newsize = self.sizeVert[0] maxsize = newsize + (rcc[2] - rcc[0]) minsize = self.sizeMin[0] pt = CenterPoint(self.rectTracker) if self.nDockBarID == afxres.AFX_IDW_DOCKBAR_TOP: newsize = newsize + (pt[1] - self.ptOld[1]) elif self.nDockBarID == afxres.AFX_IDW_DOCKBAR_BOTTOM: newsize = newsize + (-pt[1] + self.ptOld[1]) elif self.nDockBarID == afxres.AFX_IDW_DOCKBAR_LEFT: newsize = newsize + (pt[0] - self.ptOld[0]) elif self.nDockBarID == afxres.AFX_IDW_DOCKBAR_RIGHT: newsize = newsize + (-pt[0] + self.ptOld[0]) newsize = max(minsize, min(maxsize, newsize)) if self.IsHorz(): self.sizeHorz = self.sizeHorz[0], newsize else: self.sizeVert = newsize, self.sizeVert[1] self.dockSite.RecalcLayout() return 0 def OnInvertTracker(self, rect): assert rect[2] - rect[0] > 0 and rect[3] - rect[1] > 0, "rect is empty" assert self.bTracking rcc = self.GetWindowRect() rcf = self.dockSite.GetWindowRect() rect = OffsetRect(rect, (rcc[0] - rcf[0], rcc[1] - rcf[1])) rect = DeflateRect(rect, (1, 1)) flags = win32con.DCX_WINDOW | win32con.DCX_CACHE | win32con.DCX_LOCKWINDOWUPDATE dc = self.dockSite.GetDCEx(None, flags) try: brush = win32ui.GetHalftoneBrush() oldBrush = dc.SelectObject(brush) dc.PatBlt( (rect[0], rect[1]), (rect[2] - rect[0], rect[3] - rect[1]), win32con.PATINVERT, ) dc.SelectObject(oldBrush) finally: self.dockSite.ReleaseDC(dc) def IsHorz(self): return ( self.nDockBarID == afxres.AFX_IDW_DOCKBAR_TOP or self.nDockBarID == afxres.AFX_IDW_DOCKBAR_BOTTOM ) def ClientToWnd(self, pt): x, y = pt if self.nDockBarID == afxres.AFX_IDW_DOCKBAR_BOTTOM: y = y + self.cxEdge elif self.nDockBarID == afxres.AFX_IDW_DOCKBAR_RIGHT: x = x + self.cxEdge return x, y def DrawGripper(self, dc): # no gripper if floating if self._obj_.dwStyle & afxres.CBRS_FLOATING: return # -==HACK==- # in order to calculate the client area properly after docking, # the client area must be recalculated twice (I have no idea why) self.dockSite.RecalcLayout() # -==END HACK==- gripper = self.GetWindowRect() gripper = self.ScreenToClient(gripper) gripper = OffsetRect(gripper, (-gripper[0], -gripper[1])) gl, gt, gr, gb = gripper if self._obj_.dwStyle & afxres.CBRS_ORIENT_HORZ: # gripper at left self.rectGripper = gl, gt + 40, gl + 20, gb # draw close box self.rectClose = gl + 7, gt + 10, gl + 19, gt + 22 dc.DrawFrameControl( self.rectClose, win32con.DFC_CAPTION, win32con.DFCS_CAPTIONCLOSE ) # draw docking toggle box self.rectUndock = OffsetRect(self.rectClose, (0, 13)) dc.DrawFrameControl( self.rectUndock, win32con.DFC_CAPTION, win32con.DFCS_CAPTIONMAX ) gt = gt + 38 gb = gb - 10 gl = gl + 10 gr = gl + 3 gripper = gl, gt, gr, gb dc.Draw3dRect(gripper, clrBtnHilight, clrBtnShadow) dc.Draw3dRect(OffsetRect(gripper, (4, 0)), clrBtnHilight, clrBtnShadow) else: # gripper at top self.rectGripper = gl, gt, gr - 40, gt + 20 # draw close box self.rectClose = gr - 21, gt + 7, gr - 10, gt + 18 dc.DrawFrameControl( self.rectClose, win32con.DFC_CAPTION, win32con.DFCS_CAPTIONCLOSE ) # draw docking toggle box self.rectUndock = OffsetRect(self.rectClose, (-13, 0)) dc.DrawFrameControl( self.rectUndock, win32con.DFC_CAPTION, win32con.DFCS_CAPTIONMAX ) gr = gr - 38 gl = gl + 10 gt = gt + 10 gb = gt + 3 gripper = gl, gt, gr, gb dc.Draw3dRect(gripper, clrBtnHilight, clrBtnShadow) dc.Draw3dRect(OffsetRect(gripper, (0, 4)), clrBtnHilight, clrBtnShadow) def HookMessages(self): self.HookMessage(self.OnLButtonUp, win32con.WM_LBUTTONUP) self.HookMessage(self.OnLButtonDown, win32con.WM_LBUTTONDOWN) self.HookMessage(self.OnLButtonDblClk, win32con.WM_LBUTTONDBLCLK) self.HookMessage(self.OnNcLButtonDown, win32con.WM_NCLBUTTONDOWN) self.HookMessage(self.OnNcLButtonDblClk, win32con.WM_NCLBUTTONDBLCLK) self.HookMessage(self.OnMouseMove, win32con.WM_MOUSEMOVE) self.HookMessage(self.OnNcPaint, win32con.WM_NCPAINT) self.HookMessage(self.OnCaptureChanged, win32con.WM_CAPTURECHANGED) self.HookMessage(self.OnWindowPosChanged, win32con.WM_WINDOWPOSCHANGED) # self.HookMessage(self.OnSize, win32con.WM_SIZE) def EditCreator(parent): d = win32ui.CreateEdit() es = ( win32con.WS_CHILD | win32con.WS_VISIBLE | win32con.WS_BORDER | win32con.ES_MULTILINE | win32con.ES_WANTRETURN ) d.CreateWindow(es, (0, 0, 150, 150), parent, 1000) return d def test(): import pywin.mfc.dialog global bar bar = DockingBar() creator = EditCreator bar.CreateWindow(win32ui.GetMainFrame(), creator, "Coolbar Demo", 0xFFFFF) # win32ui.GetMainFrame().ShowControlBar(bar, 1, 0) bar.SetBarStyle( bar.GetBarStyle() | afxres.CBRS_TOOLTIPS | afxres.CBRS_FLYBY | afxres.CBRS_SIZE_DYNAMIC ) bar.EnableDocking(afxres.CBRS_ALIGN_ANY) win32ui.GetMainFrame().DockControlBar(bar, afxres.AFX_IDW_DOCKBAR_BOTTOM) if __name__ == "__main__": test()