duilib里面的relativepos设计的感觉不是很好用,采用CAnchorCtrl的模式以及C#窗口设计的模式,我们重新把相对位置的概念定义为“锚”。
如图所示,锚可以设置为相对父窗口:顶部,底部,左侧,右侧,以及它们中间的任意组合:
enum {
Left = 1, Top = 2, Right = 4, Bottom = 8,
LeftTop = Left | Top,
LeftRight = Left | Right,
LeftBottom = Left | Bottom,
TopRight = Top | Right,
TopBottom = Top | Bottom,
RightBottom = Right | Bottom,
LeftTopRight = Left | Top | Right,
LeftTopBottom = Left | Top | Bottom,
LeftRightBottom = Left | Right | Bottom,
TopRightBottom = Top | Right | Bottom,
LeftTopRightBottom = Left | Top | Right | Bottom,
};
删除与相对位置相关的函数及调用:
virtual void SetRelativePos(SIZE szMove,SIZE szZoom);
virtual void SetRelativeParentSize(SIZE sz);
virtual TRelativePosUI GetRelativePos() const;
virtual bool IsRelativePos() const;
::ZeroMemory(&m_tRelativePos, sizeof(TRelativePosUI));
增加成员变量及函数:
int GetAnchorMode();
void SetAnchorMode(int nAnchorMode);
public:
int m_nAnchorMode;
int m_nMarginLeft;
int m_nMarginTop;
int m_nMarginRight;
int m_nMarginBottom;
int m_nWidth;
int m_nHeight;
函数实现(注意成员变量的初始化):
int CControlUI::GetAnchorMode() {
return m_nAnchorMode;
}
void CControlUI::SetAnchorMode(int nAnchorMode) {
m_nAnchorMode = nAnchorMode;
}
把属性相关的先改掉。
修改属性时:
LRESULT CPropertiesWnd::OnUIPropChanged(WPARAM wp, LPARAM lp) {
CMFCPropertyGridProperty* pProp = (CMFCPropertyGridProperty *)lp;
SetUIValue(pProp, pProp->GetData());
return TRUE;
}
进而追踪:
CPropertiesWnd::SetUIValue
pControl->SetAttribute(strName,strNewVal);
CControlUI::SetAttribute
elseif( _tcscmp(pstrName, _T("relativepos")) == 0 ) {
SIZE szMove,szZoom;
LPTSTR pstr = NULL;
szMove.cx = _tcstol(pstrValue, &pstr, 10); ASSERT(pstr);
szMove.cy = _tcstol(pstr + 1, &pstr, 10); ASSERT(pstr);
szZoom.cx = _tcstol(pstr + 1, &pstr, 10); ASSERT(pstr);
szZoom.cy = _tcstol(pstr + 1, &pstr, 10); ASSERT(pstr);
SetRelativePos(szMove,szZoom);
}
修改为:
elseif( _tcscmp(pstrName, _T("anchor")) == 0 ) {
LPTSTR pstr = NULL;
int nAnchorMode = _tcstol(pstrValue, &pstr, 10);
SetAnchorMode(nAnchorMode);
}
保存属性时的代码:
CLayoutManager::SaveSkinFile
CLayoutManager::SaveProperties
CLayoutManager::SaveControlProperty
TRelativePosUI relativePos = pControl->GetRelativePos();
if(relativePos.bRelative)
{
_stprintf_s(szBuf, _T("%d,%d,%d,%d"), relativePos.nMoveXPercent, relativePos.nMoveYPercent, relativePos.nZoomXPercent, relativePos.nZoomYPercent);
pNode->SetAttribute("relativepos", StringConvertor::WideToUtf8(szBuf));
}
修改为(0的时候就不保存了,还可以减小xml的体积):
int nAnchorMode = pControl->GetAnchorMode();
if ( nAnchorMode > 0 ) {
pNode->SetAttribute("anchor", nAnchorMode);
}
属性编辑器的初始化代码:
pValueList = new CMFCPropertyGridProperty(_T("RelativePos"),tagRelativePos,TRUE);//relativepos
pProp=new CMFCPropertyGridProperty(_T("MoveX"),(_variant_t)(LONG)0,_T("控件的水平位移"));
pValueList->AddSubItem(pProp);
pProp=new CMFCPropertyGridProperty(_T("MoveY"),(_variant_t)(LONG)0,_T("控件的垂直位移"));
pValueList->AddSubItem(pProp);
pProp=new CMFCPropertyGridProperty(_T("ZoomX"),(_variant_t)(LONG)0,_T("控件的水平比例"));
pValueList->AddSubItem(pProp);
pProp=new CMFCPropertyGridProperty(_T("ZoomY"),(_variant_t)(LONG)0,_T("控件的垂直比例"));
pValueList->AddSubItem(pProp);
pPropUI->AddSubItem(pValueList);
修改为:
pProp=new CMFCPropertyGridProperty(_T("Anchor"),(_variant_t)(LONG)0,_T("控件相对位置"),tagAnchor);
pPropUI->AddSubItem(pProp);
属性显示:
//relativepos
TRelativePosUI posRelative=pControl->GetRelativePos();
pValueList=pPropControl->GetSubItem(tagRelativePos-tagControl);
pValueList->GetSubItem(0)->SetValue((_variant_t)(LONG)posRelative.nMoveXPercent);
pValueList->GetSubItem(1)->SetValue((_variant_t)(LONG)posRelative.nMoveYPercent);
pValueList->GetSubItem(2)->SetValue((_variant_t)(LONG)posRelative.nZoomXPercent);
pValueList->GetSubItem(3)->SetValue((_variant_t)(LONG)posRelative.nZoomYPercent);
pValueList->GetSubItem(0)->SetOriginalValue((_variant_t)(LONG)posRelative.nMoveXPercent);
pValueList->GetSubItem(1)->SetOriginalValue((_variant_t)(LONG)posRelative.nMoveYPercent);
pValueList->GetSubItem(2)->SetOriginalValue((_variant_t)(LONG)posRelative.nZoomXPercent);
pValueList->GetSubItem(3)->SetOriginalValue((_variant_t)(LONG)posRelative.nZoomYPercent);
修改为:
//anchor
pPropControl->GetSubItem(tagAnchor-tagControl)->SetValue((_variant_t)(LONG)pControl->GetAnchorMode());
pPropControl->GetSubItem(tagAnchor-tagControl)->SetOriginalValue((_variant_t)(LONG)pControl->GetAnchorMode());
这里需要把tagRelativePos修改为tagAnchor。
上面只是把属性界面部分相关的改掉了,但是要想显示出效果,还需要修改下设置控件位置的代码,设置相对位置有两个比较重要的地方:
- 记录设计时候的初始位置。
- 在窗口resize时根据初始位置和锚来计算新位置。
由于duilib在实际工程代码中和DuiDesigner不加区分地都调用了设置控件位置的函数:
SetPos和SetFloatPos(实际上Float属性在引入了锚的概念后也可以一并砍掉,但是为了降低复杂地,一次只优化一个节点)。
例如在资源编辑时,拖动控件:
CUIDesignerView::OnLButtonDown
CMultiUITracker::Track
CMultiUITracker::MultiTrackHandle
CMultiUITracker::UpdateUIRect
CUIDesignerView::Notify
CHorizontalLayoutUI::SetPos
CContainerUI::SetFloatPos
因为编辑资源时和运行时设置控件位置的函数一样,这里考虑在SetPos和SetFloatPos中增加一个参数用于区分:BOOL bInitedData,在资源编辑时设置的bInitedData均设为TRUE,其他情况皆为FALSE。并不设定默认的参数,这一点是为了后面修改方便。
首先,只在duilib项目里把所有有关SetPos和SetFloatPos的声明的函数都添加上以上参数(派生类重载的方法有很多),修改完成后编译duilib,找到出错的代码一个个添加缺失的参数,大部分是设置的FALSE。
偷巧的一点:因为在DuiDesigner中手动修改的控件坐标都认定为第一次设置,也就是bInitedData为TRUE,在成功编译duilib后再单独编译DuiDesigner,凡是出现错误的地方一律添加缺失的参数,基本上都是TRUE。
尤其是CUIDesignerView被加载的时候:
CUIDesignerView::OnInitialUpdate()
CLayoutManager::Init
CWindowUI::SetInitSize
CWindowUI::SetPos
CControlUI::SetPos
最后修改的函数如下:
void CControlUI::SetPos(RECT rc, BOOL bInitedData)
{
if( rc.right < rc.left ) rc.right = rc.left;
if( rc.bottom < rc.top ) rc.bottom = rc.top;
CDuiRect invalidateRc = m_rcItem;
if( ::IsRectEmpty(&invalidateRc) ) invalidateRc = rc;
m_rcItem = rc;
if( m_pManager == NULL ) return;
if( !m_bSetPos ) {
m_bSetPos = true;
if( OnSize ) OnSize(this);
m_bSetPos = false;
}
CControlUI* pParent = GetParent();
if ( pParent==NULL ) {
return;
}
RECT rcParentPos = pParent->GetPos();
if ( bInitedData==TRUE ) {
m_nMarginLeft = m_rcItem.left - rcParentPos.left;
m_nMarginTop = m_rcItem.top - rcParentPos.top;
m_nMarginRight = m_rcItem.right - rcParentPos.right;
m_nMarginBottom = m_rcItem.bottom - rcParentPos.bottom;
m_nWidth = m_rcItem.right - m_rcItem.left;
m_nHeight = m_rcItem.bottom - m_rcItem.top;
}if( m_bFloat ) {
if( m_cXY.cx >= 0 ) m_cXY.cx = m_rcItem.left - rcParentPos.left;
else m_cXY.cx = m_rcItem.right - rcParentPos.right;
if( m_cXY.cy >= 0 ) m_cXY.cy = m_rcItem.top - rcParentPos.top;
else m_cXY.cy = m_rcItem.bottom - rcParentPos.bottom;
m_cxyFixed.cx = m_rcItem.right - m_rcItem.left;
m_cxyFixed.cy = m_rcItem.bottom - m_rcItem.top;
}
m_bUpdateNeeded = false;
invalidateRc.Join(m_rcItem);
pParent = this;
RECT rcTemp;
RECT rcParent;
while( pParent = pParent->GetParent() )
{
rcTemp = invalidateRc;
rcParent = pParent->GetPos();
if( !::IntersectRect(&invalidateRc, &rcTemp, &rcParent) )
{
return;
}
}
m_pManager->Invalidate(invalidateRc);
}
也就是在初始化的时候记录相对父控件的上下左右的距离和宽高,以方便后面窗口大小发生变化的时候使用。
下面是一个小插曲,很多时候在你解决问题的时候往往会穿插或者伴随着别的问题,诸多问题连结在一起的时候怎么办?
以RichListRes为例,用修改后的DuiDesigner重新载入xml皮肤配置文件,稍作修改并撤销然后保存,为的是重新保存xml文件,运行RichListRes工程会崩溃。
然后发现DuiDesigner用的是tinyxml保存的xml,而加载的时候用的是CMarkup,以至于以为是两者操作xml的不同导致的问题,当时差点就要把这两者的操作改为同一个库了。实际上这是不可取的,毕竟工作量太大了。然后用DuiDesigner关闭当前xml并重新载入,发现也不能正常显示了。
通过SVN找BUG:DuiDesigner修改前后的xml通过svn进行对比,并逐块恢复,最终定位到导致出错的xml代码:
原:
<Default name="VScrollBar" value="button1normalimage="file='scrollbar.bmp' source='0,90,16,106' mask='#FFFF00FF'" button1hotimage="file='scrollbar.bmp' source='18,90,34,106' mask='#FFFF00FF'" button1pushedimage="file='scrollbar.bmp' source='36,90,52,106' mask='#FFFF00FF'" button1disabledimage="file='scrollbar.bmp' source='54,90,70,106' mask='#FFFF00FF'" button2normalimage="file='scrollbar.bmp' source='0,108,16,124' mask='#FFFF00FF'" button2hotimage="file='scrollbar.bmp' source='18,108,34,124' mask='#FFFF00FF'" button2pushedimage="file='scrollbar.bmp' source='36,108,52,124' mask='#FFFF00FF'" button2disabledimage="file='scrollbar.bmp' source='54,108,70,124' mask='#FFFF00FF'" thumbnormalimage="file='scrollbar.bmp' source='0,126,16,142' corner='2,2,2,2' mask='#FFFF00FF'" thumbhotimage="file='scrollbar.bmp' source='18,126,34,142' corner='2,2,2,2' mask='#FFFF00FF'" thumbpushedimage="file='scrollbar.bmp' source='36,126,52,142' corner='2,2,2,2' mask='#FFFF00FF'" thumbdisabledimage="file='scrollbar.bmp' source='54,126,70,142' corner='2,2,2,2' mask='#FFFF00FF'" railnormalimage="file='scrollbar.bmp' source='0,144,16,160' corner='2,2,2,2' mask='#FFFF00FF'" railhotimage="file='scrollbar.bmp' source='18,144,34,160' corner='2,2,2,2' mask='#FFFF00FF'" railpushedimage="file='scrollbar.bmp' source='36,144,52,160' corner='2,2,2,2' mask='#FFFF00FF'" raildisabledimage="file='scrollbar.bmp' source='54,144,70,160' corner='2,2,2,2' mask='#FFFF00FF'" bknormalimage="file='scrollbar.bmp' source='0,162,16,178' corner='2,2,2,2' mask='#FFFF00FF'" bkhotimage="file='scrollbar.bmp' source='18,162,34,178' corner='2,2,2,2' mask='#FFFF00FF'" bkpushedimage="file='scrollbar.bmp' source='36,162,52,178' corner='2,2,2,2' mask='#FFFF00FF'" bkdisabledimage="file='scrollbar.bmp' source='54,162,70,178' corner='2,2,2,2' mask='#FFFF00FF'" "/>
改:
<Default name="VScrollBar" value='button1normalimage="file='scrollbar.bmp' source='0,90,16,106' mask='#FFFF00FF'" button1hotimage="file='scrollbar.bmp' source='18,90,34,106' mask='#FFFF00FF'" button1pushedimage="file='scrollbar.bmp' source='36,90,52,106' mask='#FFFF00FF'" button1disabledimage="file='scrollbar.bmp' source='54,90,70,106' mask='#FFFF00FF'" button2normalimage="file='scrollbar.bmp' source='0,108,16,124' mask='#FFFF00FF'" button2hotimage="file='scrollbar.bmp' source='18,108,34,124' mask='#FFFF00FF'" button2pushedimage="file='scrollbar.bmp' source='36,108,52,124' mask='#FFFF00FF'" button2disabledimage="file='scrollbar.bmp' source='54,108,70,124' mask='#FFFF00FF'" thumbnormalimage="file='scrollbar.bmp' source='0,126,16,142' corner='2,2,2,2' mask='#FFFF00FF'" thumbhotimage="file='scrollbar.bmp' source='18,126,34,142' corner='2,2,2,2' mask='#FFFF00FF'" thumbpushedimage="file='scrollbar.bmp' source='36,126,52,142' corner='2,2,2,2' mask='#FFFF00FF'" thumbdisabledimage="file='scrollbar.bmp' source='54,126,70,142' corner='2,2,2,2' mask='#FFFF00FF'" railnormalimage="file='scrollbar.bmp' source='0,144,16,160' corner='2,2,2,2' mask='#FFFF00FF'" railhotimage="file='scrollbar.bmp' source='18,144,34,160' corner='2,2,2,2' mask='#FFFF00FF'" railpushedimage="file='scrollbar.bmp' source='36,144,52,160' corner='2,2,2,2' mask='#FFFF00FF'" raildisabledimage="file='scrollbar.bmp' source='54,144,70,160' corner='2,2,2,2' mask='#FFFF00FF'" bknormalimage="file='scrollbar.bmp' source='0,162,16,178' corner='2,2,2,2' mask='#FFFF00FF'" bkhotimage="file='scrollbar.bmp' source='18,162,34,178' corner='2,2,2,2' mask='#FFFF00FF'" bkpushedimage="file='scrollbar.bmp' source='36,162,52,178' corner='2,2,2,2' mask='#FFFF00FF'" bkdisabledimage="file='scrollbar.bmp' source='54,162,70,178' corner='2,2,2,2' mask='#FFFF00FF'" '/>
可以看出问题来了,也就是value值的部分是用的单引号而非双引号,导致后面CMarkup解析出错。下面就要排查tinyxml在保存文件时候的代码逻辑。
在CLayoutManager::SaveSkinFile函数中找到以下代码:
const CStdStringPtrMap& defaultAttrHash = m_Manager.GetDefaultAttribultes();
if(defaultAttrHash.GetSize() > 0)
{
for (int index = 0; index < defaultAttrHash.GetSize(); ++index)
{
LPCTSTR lpstrKey = defaultAttrHash.GetAt(index);
LPCTSTR lpstrAttribute = m_Manager.GetDefaultAttributeList(lpstrKey);
TiXmlElement* pAttributeElem = new TiXmlElement("Default");
pAttributeElem->SetAttribute("name", StringConvertor::WideToUtf8(lpstrKey));
CString strAttrib(lpstrAttribute);
pAttributeElem->SetAttribute("value", StringConvertor::WideToUtf8(strAttrib));
pNode->ToElement()->InsertEndChild(*pAttributeElem);
delete pAttributeElem;
pAttributeElem = NULL;
}
}
也正是保存上面那段xml代码的地方。一直跟进到value被保存的代码:
void TiXmlAttribute::Print( FILE* cfile, int/*depth*/, TIXML_STRING* str ) const
{
TIXML_STRING n, v;
EncodeString( name, &n );
EncodeString( value, &v );
if (value.find ('\"') == TIXML_STRING::npos) {
if ( cfile ) {
fprintf (cfile, "%s=\"%s\"", n.c_str(), v.c_str() );
}
if ( str ) {
(*str) += n; (*str) += "=\""; (*str) += v; (*str) += "\"";
}
}
else {
if ( cfile ) {
fprintf (cfile, "%s='%s'", n.c_str(), v.c_str() );
}
if ( str ) {
(*str) += n; (*str) += "='"; (*str) += v; (*str) += "'";
}
}
}
从这里就能发现问题了,EncodeString会把value处理一次,会把字符串里面出现的引号给转义掉生成新的v,后面的代码:
if (value.find ('\"') == TIXML_STRING::npos)
是有bug的,应该是用v比较而不是用value比较,所以就被添加了单引号进去。后来想起,tinyxml被我用最新版的代码替换过一次,可以通过svn恢复:
void TiXmlAttribute::Print( FILE* cfile, int/*depth*/, TIXML_STRING* str ) const
{
TIXML_STRING n, v;
EncodeString( name, &n );
EncodeString( value, &v );
if ( cfile ) {
fprintf (cfile, "%s=\"%s\"", n.c_str(), v.c_str() );
}
if ( str ) {
(*str) += n; (*str) += "=\""; (*str) += v; (*str) += "\"";
}
}
重新编译后崩溃消除,看来最新的代码也不见得最稳定啊。
解决掉上述问题后继续,在duilib里面作为父容器的有:CContainerUI、CHorizontalLayoutUI、CVerticalLayoutUI、CTileLayoutUI、CTabLayoutUI、CChildLayoutUI等。后面的几个layout均派生自CContainerUI。
这里修改下CContainerUI的SetPos:
void CContainerUI::SetPos(RECT rc, BOOL bInitedData)
{
CControlUI::SetPos(rc, bInitedData);
if( m_items.IsEmpty() ) return;
rc.left += m_rcInset.left;
rc.top += m_rcInset.top;
rc.right -= m_rcInset.right;
rc.bottom -= m_rcInset.bottom;
RECT rcCtrl = rc;
for( int it = 0; it < m_items.GetSize(); it++ ) {
CControlUI* pControl = static_cast<CControlUI*>(m_items[it]);
if( !pControl->IsVisible() ) continue;
if( pControl->IsFloat() ) {
SetFloatPos(it, bInitedData);
}
else {
int nAnchorMode = pControl->GetAnchorMode();
if ( nAnchorMode >0 ) {
DWORD dwFlag = nAnchorMode;
if ( dwFlag & Left ) {
rcCtrl.left = rc.left + pControl->m_nMarginLeft;
if ( dwFlag & Right ) {
rcCtrl.right = rc.right + pControl->m_nMarginRight;
}else{
rcCtrl.right = rcCtrl.left + m_nWidth;
}
}else{
if ( dwFlag & Right ) {
rcCtrl.right = rc.right + pControl->m_nMarginRight;
}
rcCtrl.left = rcCtrl.right - m_nWidth;
}
if ( dwFlag & Top ) {
rcCtrl.top = rc.top + m_nMarginTop;
if ( dwFlag & Bottom ) {
rcCtrl.bottom = rc.bottom + pControl->m_nMarginBottom;
}else{
rcCtrl.bottom = rcCtrl.top + m_nHeight;
}
}else{
if ( dwFlag & Bottom ) {
rcCtrl.bottom = rc.bottom + pControl->m_nMarginBottom;
}
rcCtrl.top = rcCtrl.bottom - m_nHeight;
}
}
pControl->SetPos(rcCtrl, bInitedData); // 所有非float子控件放大到整个客户区 }
}
}
这段代码在Anchor设置为0的时候同原逻辑一样,因此不会影响原来皮肤的使用。
其他几个容器都有重载SetPos,太多了不想改了。目前的这个版本兼容之前的皮肤使用。
总结:
CContainerUI作为填充容器,默认撑满其余空间,这一点要好好利用。CContainerUI放在
例如在一个HorizontalLayout里的控件,子控件的Float属性设置为false则会自动排列,这一点也要好好利用。