// ResizableLayout.cpp: implementation of the CResizableLayout class. // ///////////////////////////////////////////////////////////////////////////// // // Copyright (C) 2000-2002 by Paolo Messina // (http://www.geocities.com/ppescher - ppescher@yahoo.com) // // The contents of this file are subject to the Artistic License (the "License"). // You may not use this file except in compliance with the License. // You may obtain a copy of the License at: // http://www.opensource.org/licenses/artistic-license.html // // If you find this code useful, credits would be nice! // ///////////////////////////////////////////////////////////////////////////// #include "stdafx.h" #include "ResizableLayout.h" #include "ResizableMsgSupport.inl" #ifdef _DEBUG #undef THIS_FILE static char THIS_FILE[]=__FILE__; #define new DEBUG_NEW #endif ////////////////////////////////////////////////////////////////////// // Construction/Destruction ////////////////////////////////////////////////////////////////////// // In August 2002 Platform SDK, some guy at MS thought it was time to // add the missing symbol BS_TYPEMASK, but forgot its original meaning // and so now he's telling us not to use that symbol because its // value is likely to change in the future SDK releases, including all // the BS_* style bits in the mask, not just the button's type as the // symbol's name suggests. So now we're forced to use another symbol! #define _BS_TYPEMASK 0x0000000FL void CResizableLayout::AddAnchor(HWND hWnd, CSize sizeTypeTL, CSize sizeTypeBR) { CWnd* pParent = GetResizableWnd(); // child window must be valid ASSERT(::IsWindow(hWnd)); // must be child of parent window ASSERT(::IsChild(pParent->GetSafeHwnd(), hWnd)); // top-left anchor must be valid ASSERT(sizeTypeTL != NOANCHOR); // get control's window class CString sClassName; GetClassName(hWnd, sClassName.GetBufferSetLength(MAX_PATH), MAX_PATH); sClassName.ReleaseBuffer(); // get parent window's rect CRect rectParent; GetTotalClientRect(&rectParent); // and child control's rect CRect rectChild; ::GetWindowRect(hWnd, &rectChild); ::MapWindowPoints(NULL, pParent->m_hWnd, (LPPOINT)&rectChild, 2); // adjust position, if client area has been scrolled rectChild.OffsetRect(-rectParent.TopLeft()); // go calculate margins CSize sizeMarginTL, sizeMarginBR; if (sizeTypeBR == NOANCHOR) sizeTypeBR = sizeTypeTL; // calculate margin for the top-left corner sizeMarginTL.cx = rectChild.left - rectParent.Width() * sizeTypeTL.cx / 100; sizeMarginTL.cy = rectChild.top - rectParent.Height() * sizeTypeTL.cy / 100; // calculate margin for the bottom-right corner sizeMarginBR.cx = rectChild.right - rectParent.Width() * sizeTypeBR.cx / 100; sizeMarginBR.cy = rectChild.bottom - rectParent.Height() * sizeTypeBR.cy / 100; // prepare the structure LayoutInfo layout(hWnd, sizeTypeTL, sizeMarginTL, sizeTypeBR, sizeMarginBR, sClassName); // initialize resize properties (overridable) InitResizeProperties(layout); // must not be already there! // (this is probably due to a duplicate call to AddAnchor) POSITION pos; ASSERT(!m_mapLayout.Lookup(hWnd, pos)); // add to the list and the map pos = m_listLayout.AddTail(layout); m_mapLayout.SetAt(hWnd, pos); } void CResizableLayout::AddAnchorCallback(UINT nCallbackID) { // one callback control cannot rely upon another callback control's // size and/or position (they're updated all together at the end) // it can however use a non-callback control, which is updated before // add to the list LayoutInfo layout; layout.nCallbackID = nCallbackID; m_listLayoutCB.AddTail(layout); } BOOL CResizableLayout::ArrangeLayoutCallback(CResizableLayout::LayoutInfo& /*layout*/) { ASSERT(FALSE); // must be overridden, if callback is used return FALSE; // no output data } void CResizableLayout::ArrangeLayout() { // common vars UINT uFlags; LayoutInfo layout; CRect rectParent, rectChild; GetTotalClientRect(&rectParent); // get parent window's rect int count = m_listLayout.GetCount(); int countCB = m_listLayoutCB.GetCount(); // reposition child windows HDWP hdwp = ::BeginDeferWindowPos(count + countCB); POSITION pos = m_listLayout.GetHeadPosition(); while (pos != NULL) { // get layout info layout = m_listLayout.GetNext(pos); // calculate new child's position, size and flags for SetWindowPos CalcNewChildPosition(layout, rectParent, rectChild, uFlags); // only if size or position changed if ((uFlags & (SWP_NOMOVE|SWP_NOSIZE)) != (SWP_NOMOVE|SWP_NOSIZE)) { hdwp = ::DeferWindowPos(hdwp, layout.hWnd, NULL, rectChild.left, rectChild.top, rectChild.Width(), rectChild.Height(), uFlags); } } // for callback items you may use GetAnchorPosition to know the // new position and size of a non-callback item after resizing pos = m_listLayoutCB.GetHeadPosition(); while (pos != NULL) { // get layout info layout = m_listLayoutCB.GetNext(pos); // request layout data if (!ArrangeLayoutCallback(layout)) continue; // calculate new child's position, size and flags for SetWindowPos CalcNewChildPosition(layout, rectParent, rectChild, uFlags); // only if size or position changed if ((uFlags & (SWP_NOMOVE|SWP_NOSIZE)) != (SWP_NOMOVE|SWP_NOSIZE)) { hdwp = ::DeferWindowPos(hdwp, layout.hWnd, NULL, rectChild.left, rectChild.top, rectChild.Width(), rectChild.Height(), uFlags); } } // finally move all the windows at once ::EndDeferWindowPos(hdwp); } void CResizableLayout::ClipChildWindow(const CResizableLayout::LayoutInfo& layout, CRgn* pRegion) { // obtain window position CRect rect; ::GetWindowRect(layout.hWnd, &rect); ::MapWindowPoints(NULL, GetResizableWnd()->m_hWnd, (LPPOINT)&rect, 2); // use window region if any CRgn rgn; rgn.CreateRectRgn(0,0,0,0); switch (::GetWindowRgn(layout.hWnd, rgn)) { case COMPLEXREGION: case SIMPLEREGION: rgn.OffsetRgn(rect.TopLeft()); break; default: rgn.SetRectRgn(&rect); } // get the clipping property BOOL bClipping = layout.properties.bAskClipping ? LikesClipping(layout) : layout.properties.bCachedLikesClipping; // modify region accordingly if (bClipping) pRegion->CombineRgn(pRegion, &rgn, RGN_DIFF); else pRegion->CombineRgn(pRegion, &rgn, RGN_OR); } void CResizableLayout::GetClippingRegion(CRgn* pRegion) { CWnd* pWnd = GetResizableWnd(); // System's default clipping area is screen's size, // not enough for max track size, for example: // if screen is 1024 x 768 and resizing border is 4 pixels, // maximized size is 1024+4*2=1032 x 768+4*2=776, // but max track size is 4 pixels bigger 1036 x 780 (don't ask me why!) // So, if you resize the window to maximum size, the last 4 pixels // are clipped out by the default clipping region, that gets created // as soon as you call clipping functions (my guess). // reset clipping region to the whole client area CRect rect; pWnd->GetClientRect(&rect); pRegion->CreateRectRgnIndirect(&rect); // clip only anchored controls LayoutInfo layout; POSITION pos = m_listLayout.GetHeadPosition(); while (pos != NULL) { // get layout info layout = m_listLayout.GetNext(pos); if (::IsWindowVisible(layout.hWnd)) ClipChildWindow(layout, pRegion); } pos = m_listLayoutCB.GetHeadPosition(); while (pos != NULL) { // get layout info layout = m_listLayoutCB.GetNext(pos); // request data if (!ArrangeLayoutCallback(layout)) continue; if (::IsWindowVisible(layout.hWnd)) ClipChildWindow(layout, pRegion); } // fix for RTL layouts (1 pixel of horz offset) if (pWnd->GetExStyle() & WS_EX_LAYOUTRTL) pRegion->OffsetRgn(-1,0); } void CResizableLayout::EraseBackground(CDC* pDC) { HWND hWnd = GetResizableWnd()->GetSafeHwnd(); // retrieve the background brush HBRUSH hBrush = NULL; // is this a dialog box? // (using class atom is quickier than using the class name) ATOM atomWndClass = (ATOM)::GetClassLong(hWnd, GCW_ATOM); if (atomWndClass == (ATOM)0x8002) { // send a message to the dialog box hBrush = (HBRUSH)::SendMessage(hWnd, WM_CTLCOLORDLG, (WPARAM)pDC->GetSafeHdc(), (LPARAM)hWnd); } else { // take the background brush from the window's class hBrush = (HBRUSH)::GetClassLong(hWnd, GCL_HBRBACKGROUND); } // fill the clipped background CRgn rgn; GetClippingRegion(&rgn); ::FillRgn(pDC->GetSafeHdc(), rgn, hBrush); } // support legacy code (will disappear in future versions) void CResizableLayout::ClipChildren(CDC* pDC) { CRgn rgn; GetClippingRegion(&rgn); // the clipping region is in device units rgn.OffsetRgn(-pDC->GetWindowOrg()); pDC->SelectClipRgn(&rgn); } void CResizableLayout::GetTotalClientRect(LPRECT lpRect) { GetResizableWnd()->GetClientRect(lpRect); } BOOL CResizableLayout::NeedsRefresh(const CResizableLayout::LayoutInfo& layout, const CRect& rectOld, const CRect& rectNew) { if (layout.bMsgSupport) { REFRESHPROPERTY refresh; refresh.rcOld = rectOld; refresh.rcNew = rectNew; if (Send_NeedsRefresh(layout.hWnd, &refresh)) return refresh.bNeedsRefresh; } int nDiffWidth = (rectNew.Width() - rectOld.Width()); int nDiffHeight = (rectNew.Height() - rectOld.Height()); // is the same size? if (nDiffWidth == 0 && nDiffHeight == 0) return FALSE; // optimistic, no need to refresh BOOL bRefresh = FALSE; // window classes that need refresh when resized if (layout.sWndClass == WC_STATIC) { DWORD style = ::GetWindowLong(layout.hWnd, GWL_STYLE); switch (style & SS_TYPEMASK) { case SS_LEFT: case SS_CENTER: case SS_RIGHT: // word-wrapped text bRefresh = bRefresh || (nDiffWidth != 0); // vertically centered text if (style & SS_CENTERIMAGE) bRefresh = bRefresh || (nDiffHeight != 0); break; case SS_LEFTNOWORDWRAP: // text with ellipsis if (style & SS_ELLIPSISMASK) bRefresh = bRefresh || (nDiffWidth != 0); // vertically centered text if (style & SS_CENTERIMAGE) bRefresh = bRefresh || (nDiffHeight != 0); break; case SS_ENHMETAFILE: case SS_BITMAP: case SS_ICON: // images case SS_BLACKFRAME: case SS_GRAYFRAME: case SS_WHITEFRAME: case SS_ETCHEDFRAME: // and frames bRefresh = TRUE; break; } } // window classes that don't redraw client area correctly // when the hor scroll pos changes due to a resizing BOOL bHScroll = FALSE; if (layout.sWndClass == WC_LISTBOX) bHScroll = TRUE; // fix for horizontally scrollable windows if (bHScroll && (nDiffWidth > 0)) { // get max scroll position SCROLLINFO info; info.cbSize = sizeof(SCROLLINFO); info.fMask = SIF_PAGE | SIF_POS | SIF_RANGE; if (::GetScrollInfo(layout.hWnd, SB_HORZ, &info)) { // subtract the page size info.nMax -= __max(info.nPage-1,0); } // resizing will cause the text to scroll on the right // because the scrollbar is going beyond the right limit if ((info.nMax > 0) && (info.nPos + nDiffWidth > info.nMax)) { // needs repainting, due to horiz scrolling bRefresh = TRUE; } } return bRefresh; } BOOL CResizableLayout::LikesClipping(const CResizableLayout::LayoutInfo& layout) { if (layout.bMsgSupport) { CLIPPINGPROPERTY clipping; if (Send_LikesClipping(layout.hWnd, &clipping)) return clipping.bLikesClipping; } DWORD style = ::GetWindowLong(layout.hWnd, GWL_STYLE); // skip windows that wants background repainted if (layout.sWndClass == TOOLBARCLASSNAME && (style & TBSTYLE_TRANSPARENT)) return FALSE; else if (layout.sWndClass == WC_BUTTON) { CRect rect; switch (style & _BS_TYPEMASK) { case BS_GROUPBOX: return FALSE; case BS_OWNERDRAW: // ownerdraw buttons must return correct hittest code // to notify their transparency to the system and this library ::GetWindowRect(layout.hWnd, &rect); if ( HTTRANSPARENT == ::SendMessage(layout.hWnd, WM_NCHITTEST, 0, MAKELPARAM(rect.left, rect.top)) ) return FALSE; break; } return TRUE; } else if (layout.sWndClass == WC_STATIC) { switch (style & SS_TYPEMASK) { case SS_LEFT: case SS_CENTER: case SS_RIGHT: case SS_SIMPLE: case SS_LEFTNOWORDWRAP: // text case SS_BLACKRECT: case SS_GRAYRECT: case SS_WHITERECT: // filled rects case SS_ETCHEDHORZ: case SS_ETCHEDVERT: // etched lines case SS_BITMAP: // bitmaps return TRUE; break; case SS_ICON: case SS_ENHMETAFILE: if (style & SS_CENTERIMAGE) return FALSE; return TRUE; break; default: return FALSE; } } // assume the others like clipping return TRUE; } void CResizableLayout::CalcNewChildPosition(const CResizableLayout::LayoutInfo& layout, const CRect &rectParent, CRect &rectChild, UINT& uFlags) { CWnd* pParent = GetResizableWnd(); ::GetWindowRect(layout.hWnd, &rectChild); ::MapWindowPoints(NULL, pParent->m_hWnd, (LPPOINT)&rectChild, 2); CRect rectNew; // calculate new top-left corner rectNew.left = layout.sizeMarginTL.cx + rectParent.Width() * layout.sizeTypeTL.cx / 100; rectNew.top = layout.sizeMarginTL.cy + rectParent.Height() * layout.sizeTypeTL.cy / 100; // calculate new bottom-right corner rectNew.right = layout.sizeMarginBR.cx + rectParent.Width() * layout.sizeTypeBR.cx / 100; rectNew.bottom = layout.sizeMarginBR.cy + rectParent.Height() * layout.sizeTypeBR.cy / 100; // adjust position, if client area has been scrolled rectNew.OffsetRect(rectParent.TopLeft()); // get the refresh property BOOL bRefresh = layout.properties.bAskRefresh ? NeedsRefresh(layout, rectChild, rectNew) : layout.properties.bCachedNeedsRefresh; // set flags uFlags = SWP_NOZORDER | SWP_NOACTIVATE | SWP_NOREPOSITION; if (bRefresh) uFlags |= SWP_NOCOPYBITS; if (rectNew.TopLeft() == rectChild.TopLeft()) uFlags |= SWP_NOMOVE; if (rectNew.Size() == rectChild.Size()) uFlags |= SWP_NOSIZE; // update rect rectChild = rectNew; } void CResizableLayout::InitResizeProperties(CResizableLayout::LayoutInfo &layout) { // check if custom window supports this library // (properties must be correctly set by the window) layout.bMsgSupport = Send_QueryProperties(layout.hWnd, &layout.properties); // default properties if (!layout.bMsgSupport) { // clipping property is assumed as static layout.properties.bAskClipping = FALSE; layout.properties.bCachedLikesClipping = LikesClipping(layout); // refresh property is assumed as dynamic layout.properties.bAskRefresh = TRUE; } }