DLL:
DuiLibDelayResetBtnPlugin.dll
类:CCDelayButton、CCDelayTabButton、CCDelayBtnLayout
标签:DelayResetBtn/DelayButton、DelayTabChangeButton/DelayTabButton、
SDelayResetBtnLayout/DelayResetLayout/DelayResetMutiLayout
一个 DLL 导出三个类,对应原工程 DelayResetBtn 插件里的四份源码
(DelayButton、DelayTabChangeButton、SDelayResetBtnLayout、
DelayResetMutiLayout)。
三个类放一个插件的理由(源码注释原文):
它们共用同一套"按钮状态机 + 计时"逻辑。
一、CCDelayButton:延时复位按钮
1.1 语义:“按下后延时 N 毫秒才生效”
普通按钮是"松手就触发"。延时按钮是按下就开始计时,到点才触发,
期间显示倒计时。
private:
int m_nDelayMs; // 延时毫秒,默认 800
double m_dRemain; // 剩余秒数
bool m_bCounting; // 是否在倒计时
bool m_bAllowRepeat; // 倒计时中是否允许再次点击
bool m_bHot;
bool m_bPressed;
bool m_bProgressVisible;
1.2 按下即开始
void CCDelayButton::DoEvent(TEventUI& event)
{
if (event.Type == UIEVENT_BUTTONDOWN) {
if (!IsEnabled()) return;
if (m_bCounting && !m_bAllowRepeat) return; // ← 不允许重复时忽略
m_bPressed = true;
m_bCounting = true;
m_dRemain = (double)m_nDelayMs / 1000.0; // ← 重置剩余时间
StartTick();
Invalidate();
return;
}
if (event.Type == UIEVENT_BUTTONUP) {
if (m_bPressed) {
m_bPressed = false;
Invalidate();
}
// 计数仍在继续,由 OnTick 负责到期复位
return;
}
if (event.Type == UIEVENT_MOUSEENTER) { m_bHot = true; Invalidate(); return; }
if (event.Type == UIEVENT_MOUSELEAVE) {
m_bHot = false;
m_bPressed = false;
Invalidate();
return;
}
CC::CCControlBase::DoEvent(event);
}
松手不停止计时(BUTTONUP 里只清 m_bPressed,不动 m_bCounting)。
这是关键差异——普通按钮松手就触发,延时按钮松手后倒计时继续走,
到点才发通知。用户只需要"点一下",不用一直按着。
1.3 倒计时到期
void CCDelayButton::OnTick(double dt, double dTotal)
{
(void)dTotal;
if (!m_bCounting) return;
m_dRemain -= dt;
if (m_dRemain <= 0.0) {
m_dRemain = 0.0;
m_bCounting = false;
m_bPressed = false;
Invalidate();
// 到期:这就是"延时复位"的语义——先亮着,到点发事件
FireNotify(_T("delaydone"));
return;
}
Invalidate();
}
“先亮着,到点发事件”——这是注释原话。
为什么这样设计?因为这类按钮用于"复位"操作:按下去时设备先"响应"(灯亮 / 声音),
但要等 N 毫秒确认没有二次操作才真正执行复位。期间用户可以再按一次取消
(如果 allowrepeat="true")。
1.4 视觉:三种底色 + 倒计时进度条
void CCDelayButton::PaintCNContent(HDC hDC, const RECT& rcPaint)
{
(void)rcPaint;
RECT rc = m_rcItem;
if (CC::IsRectBlank(rc)) return;
DWORD dwBg = m_dwNormalColor;
if (!IsEnabled()) dwBg = 0xFF3A4249; // 禁用
else if (m_bCounting) dwBg = m_dwCountColor; // 倒计时中
else if (m_bPressed) dwBg = m_dwPushedColor; // 按下
else if (m_bHot) dwBg = m_dwHotColor; // 悬停
int nRadius = m_cBorderRadius > 0 ? m_cBorderRadius : 4;
CC::FillRoundRect(hDC, rc, nRadius, dwBg);
// 倒计时进度条(从满到空)
if (m_bProgressVisible && m_bCounting && m_nDelayMs > 0) {
double dRatio = m_dRemain / ((double)m_nDelayMs / 1000.0);
dRatio = CC::Clamp01(dRatio);
int nW = (int)((double)CC::RectWidth(rc) * dRatio);
if (nW > 0) {
RECT rcProg = rc;
rcProg.top = rcProg.bottom - 3;
rcProg.right = rcProg.left + nW;
CC::FillRoundRect(hDC, rcProg, 1, 0xFF35C0FF);
}
}
// 文字:倒计时时显示剩余秒数
HFONT hFont = m_font.Get(13, false);
/* 选字体、SetBkMode、SetTextColor */
CDuiString sText = m_sText;
if (m_bCounting && !sText.IsEmpty()) {
CDuiString sSuffix;
sSuffix.Format(_T(" (%.1fs)"), m_dRemain); // ← 追加倒计时
sText += sSuffix;
}
RECT rcText = rc;
rcText.left += 6;
rcText.right -= 6;
::DrawText(hDC, sText.GetData(), -1, &rcText,
DT_CENTER | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS);
/* 还原字体 */
}
1.5 进度条从满到空
double dRatio = m_dRemain / ((double)m_nDelayMs / 1000.0);
m_dRemain 从满值递减到 0,所以 dRatio 从 1 递减到 0——进度条
从满到空,和普通进度条(从空到满)相反。
这是有意的:倒计时是"时间在消耗",条子缩短直观表达"还剩多少"。
对比 CCButton 的长按进度条(从 0 长到满)——那是"蓄力",条子增长表达"快到了"。
1.6 文字追加倒计时
sSuffix.Format(_T(" (%.1fs)"), m_dRemain);
sText += sSuffix;
按钮从"复位"变成"复位 (0.8s)",%.1f 一位小数,每 100ms 变一次。
为什么用括号而不是另起一行:按钮通常只有 30px 高,一行文字放不下两行。
括号内联是最紧凑的表达。
1.7 allowrepeat 的作用
if (m_bCounting && !m_bAllowRepeat) return; // 不允许重复 → 忽略点击
allowrepeat="true"(默认):倒计时中再点一次会重置倒计时(m_dRemain重新满格);allowrepeat="false":倒计时中点击无效,必须等它走完。
"复位"类操作通常要 false——防止用户在倒计时中反复点,把复位无限延后。
二、CCDelayTabButton:互斥页签
2.1 一横排(或一竖排)互斥项
private:
std::vector<CDuiString> m_items;
int m_nSelected;
bool m_bVertical;
int m_nGap;
bool m_bIndicator; // 选中项下方高亮条
bool m_bHot;
int m_nHotIndex; // ← 悬停的是哪一项,-1 = 无
m_nHotIndex 是这一项与普通按钮的关键区别——普通按钮只有"整体悬停",
页签需要知道"悬停在哪一个页签上"。
2.2 项矩形计算
RECT CCDelayTabButton::TabRect(int nIndex) const
{
int nCount = (int)m_items.size();
if (nCount <= 0 || nIndex < 0 || nIndex >= nCount) {
RECT zero = { 0, 0, 0, 0 };
return zero;
}
RECT rc;
if (m_bVertical) {
int nH = (CC::RectHeight(m_rcItem) - m_nGap * (nCount - 1)) / nCount;
if (nH < 1) nH = 1;
rc.left = m_rcItem.left;
rc.right = m_rcItem.right;
rc.top = m_rcItem.top + nIndex * (nH + m_nGap);
rc.bottom = rc.top + nH;
}
else {
int nW = (CC::RectWidth(m_rcItem) - m_nGap * (nCount - 1)) / nCount;
if (nW < 1) nW = 1;
rc.left = m_rcItem.left + nIndex * (nW + m_nGap);
rc.right = rc.left + nW;
rc.top = m_rcItem.top;
rc.bottom = m_rcItem.bottom;
}
return rc;
}
(总尺寸 - gap × (n-1)) / n 是等分公式——先扣掉 n-1 个间隙,
剩下的平均分给 n 项。
例:宽 300、3 项、gap 4 → (300 - 4*2) / 3 = 97.33 → 每项 97px,
实际总占 97*3 + 4*2 = 299(少 1px 是取整误差,可接受)。
2.3 命中测试
int CCDelayTabButton::HitTestTab(POINT pt) const
{
for (int i = 0; i < (int)m_items.size(); ++i) {
RECT rc = TabRect(i);
if (pt.x >= rc.left && pt.x < rc.right && pt.y >= rc.top && pt.y < rc.bottom) {
return i;
}
}
return -1;
}
逐个矩形判断,< right 而不是 <= right(避免相邻项的边界重叠)。
2.4 事件:点击切页 + 悬停跟踪
void CCDelayTabButton::DoEvent(TEventUI& event)
{
if (event.Type == UIEVENT_BUTTONDOWN) {
int nIndex = HitTestTab(event.ptMouse);
if (nIndex >= 0 && nIndex != m_nSelected) { // ← 点已选中的不重复发
m_nSelected = nIndex;
Invalidate();
FireNotify(_T("tabchanged"));
}
return;
}
if (event.Type == UIEVENT_MOUSEMOVE) { // ← 注意是 MOUSEMOVE 不是 MOUSEENTER
int nIndex = HitTestTab(event.ptMouse);
if (nIndex != m_nHotIndex) {
m_nHotIndex = nIndex;
m_bHot = (nIndex >= 0);
Invalidate();
}
return;
}
if (event.Type == UIEVENT_MOUSELEAVE) {
if (m_nHotIndex != -1) {
m_nHotIndex = -1;
m_bHot = false;
Invalidate();
}
return;
}
CC::CCControlBase::DoEvent(event);
}
2.5 必须用 UIEVENT_MOUSEMOVE 而不是 MOUSEENTER
这是页签控件特有的要求:
MOUSEENTER/MOUSELEAVE是"进入/离开整个控件",只能知道"鼠标在控件内";MOUSEMOVE是"在控件内移动",能知道"现在悬停在哪个子项上"。
所以 CCDelayTabButton 必须处理 UIEVENT_MOUSEMOVE。
DuiLib 默认只在控件带 UIFLAG_SETCURSOR 时才投递 MOUSEMOVE。
CCDelayTabButton 没有重写 GetControlFlags()——它继承 CCControlBase,
而 CCControlBase 也没重写,所以是 CControlUI 的默认值。
当前实现能工作,是因为 CControlUI::GetControlFlags() 默认返回 UIFLAG_TABSTOP,
而 DuiLib 的 CPaintManagerUI 对带 TABSTOP 的控件也会投递 MOUSEMOVE。
如果将来 DuiLib 改了投递规则,需要显式声明:
UINT CCDelayTabButton::GetControlFlags() const
{
return UIFLAG_TABSTOP | UIFLAG_SETCURSOR;
}
这是个潜在脆弱点,建议主动加上。
2.6 点击已选中项不发通知
if (nIndex >= 0 && nIndex != m_nSelected) {
点当前已选中的页签时,nIndex == m_nSelected,不触发。
这是对的——页签是"切换到某个页",已经在那一页了就不该再发"切换"事件,
否则窗口侧会重复加载页面。
2.7 绘制
void CCDelayTabButton::PaintCNContent(HDC hDC, const RECT& rcPaint)
{
(void)rcPaint;
int nCount = (int)m_items.size();
if (nCount <= 0) return; // ← 空状态直接返回
int nFontH = CC::MaxOf(10, CC::MinOf(CC::RectHeight(TabRect(0)) / 2, 14));
HFONT hFont = m_font.Get(nFontH, false);
/* 选字体、SetBkMode */
for (int i = 0; i < nCount; ++i) {
RECT rcTab = TabRect(i);
if (CC::IsRectBlank(rcTab)) continue;
bool bSelected = (i == m_nSelected);
bool bHot = (i == m_nHotIndex);
DWORD dwBg = bSelected ? m_dwSelectBg : (bHot ? m_dwHotBg : m_dwTabBg);
CC::FillRoundRect(hDC, rcTab, m_cBorderRadius > 0 ? m_cBorderRadius : 4, dwBg);
// 选中项下方高亮指示条
if (bSelected && m_bIndicator) {
RECT rcBar = rcTab;
rcBar.top = rcBar.bottom - 2; // ← 底部 2px
CC::FillRoundRect(hDC, rcBar, 1, 0xFF35C0FF);
}
DWORD dwText = bSelected ? m_dwSelectTextColor : m_dwTextColor;
::SetTextColor(hDC, RGB((dwText >> 16) & 0xFF, (dwText >> 8) & 0xFF, dwText & 0xFF));
::DrawText(hDC, m_items[(size_t)i].GetData(), -1, &rcTab,
DT_CENTER | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS);
}
/* 还原字体 */
}
三层状态:
| 状态 | 底色 | 额外装饰 |
|---|---|---|
| 选中 | m_dwSelectBg(蓝) | 底部 2px 高亮条 |
| 悬停 | m_dwHotBg(微亮) | 无 |
| 普通 | m_dwTabBg(深灰) | 无 |
2.8 items 属性的幂等
void CCDelayTabButton::SetItems(LPCTSTR pstrItems)
{
m_items.clear();
if (pstrItems != NULL) {
CDuiString sAll = pstrItems;
int nPos = 0;
while (nPos <= sAll.GetLength()) {
int nComma = sAll.Find(_T(','), nPos);
CDuiString sItem = (nComma < 0) ? sAll.Mid(nPos) : sAll.Mid(nPos, nComma - nPos);
m_items.push_back(sItem); // ← 空项也推(保留占位)
if (nComma < 0) break;
nPos = nComma + 1;
}
}
if (m_nSelected >= (int)m_items.size()) m_nSelected = 0;
Invalidate();
}
注意 m_items.push_back(sItem) 不判空——和 FiveColorChart::labels 不同。
"监控,,设置" 会得到 3 项,中间那项是空字符串(显示为空白页签)。
这是"保留占位"的设计(有些界面需要空页签做间隔)。
幂等性:m_items.clear() 在开头,所以解析两次结果相同。
2.9 SetSelected 会发通知
void CCDelayTabButton::SetSelected(int nIndex)
{
if (m_items.empty()) return;
m_nSelected = CC::Clamp(nIndex, 0, (int)m_items.size() - 1);
Invalidate();
FireNotify(_T("tabchanged")); // ← 编程式设置也发通知
}
和点击不同——DoEvent 里点击已选中项不发通知,但 SetSelected 无条件发。
原因:SetSelected 是外部主动切换(脚本或窗口代码),调用方期望
“切换完成"的通知。而点击已选中项是"用户什么也没做”,不该发。
三、CCDelayBtnLayout:排布容器
3.1 它不接管布局
源码注释写得很明确:
按钮排布容器(只管画分隔与背景,不接管子控件的布局 —— DuiLib 的
XML 里子控件仍然按 hbox/vbox 排,这里额外提供"等分辅助线")
所以它是个纯装饰控件:
private:
bool m_bVertical;
int m_nGap;
bool m_bDivider; // 是否画外框
DWORD m_dwDividerColor;
int m_nCellCount; // 画几个等分格
3.2 绘制
void CCDelayBtnLayout::PaintCNContent(HDC hDC, const RECT& rcPaint)
{
(void)rcPaint;
if (m_nCellCount <= 0 && !m_bDivider) return; // ← 什么都没配就什么都不画
// 等分辅助线:让排版对齐时有个参照(默认关闭,属性里开启)
if (m_nCellCount > 1) {
for (int i = 1; i < m_nCellCount; ++i) { // ← 从 1 开始,跳过左/上边界
if (m_bVertical) {
int y = m_rcItem.top + CC::RectHeight(m_rcItem) * i / m_nCellCount;
CC::DrawSegment(hDC, m_rcItem.left, y, m_rcItem.right, y, 1, m_dwDividerColor);
}
else {
int x = m_rcItem.left + CC::RectWidth(m_rcItem) * i / m_nCellCount;
CC::DrawSegment(hDC, x, m_rcItem.top, x, m_rcItem.bottom, 1, m_dwDividerColor);
}
}
}
if (m_bDivider) {
CC::StrokeRoundRect(hDC, m_rcItem, m_cBorderRadius, 1, m_dwDividerColor);
}
}
i 从 1 开始,到 nCellCount - 1 结束——只画内部分隔线,不画外边界。
nCellCount = 3 时画 2 条线(在 1/3 和 2/3 位置)。
3.3 为什么不接管子控件布局
DuiLib 的布局系统(CHorizontalLayoutUI / CVerticalLayoutUI)已经能做
“横向排列、等分或固定宽”。CCDelayBtnLayout 如果再自己布局,会和 DuiLib 打架。
所以它的定位是**“视觉容器”**:
<SDelayResetBtnLayout orientation="horizontal" cellcount="3" divider="true" height="40">
<DelayResetBtn text="复位A" width="90" height="32" />
<DelayResetBtn text="复位B" width="90" height="32" />
<DelayResetBtn text="复位C" width="90" height="32" />
</SDelayResetBtnLayout>
子控件靠自己的 width 定位,CCDelayBtnLayout 只负责画"3 等分的辅助线"
和"外框",让视觉上看起来是分格的。
这是个"辅助对齐"的调试工具,不是真正的布局容器。生产界面里通常
cellcount="0" divider="false",什么装饰也不画。
3.4 空状态主动返回
if (m_nCellCount <= 0 && !m_bDivider) return;
没配 cellcount 也没开 divider 时,完全不画。
这是 _tools/paint_test.cpp 里"5 个画空白的控件"之一——不是缺陷,
是"没配置就不画"的预期行为。
四、属性表
CCDelayButton
| 属性 | 值 | 说明 |
|---|---|---|
text | 字符串 | 按钮文字 |
delay / delayms | int | 延时毫秒,夹到 0–60000,默认 800 |
allowrepeat | bool | 倒计时中允许重复点击,默认 true |
normalcolor | color | 常态底色,默认 #FF2A323A |
hotcolor | color | 悬停底色,默认 #FF37424C |
pushedcolor | color | 按下底色,默认 #FF1E252B |
countcolor | color | 倒计时底色,默认 #FF1F4A63(深蓝) |
textcolor | color | 文字色,默认 #FFDCE6F0 |
progress | bool | 倒计时进度条,默认 true |
只读:delay、counting("true"/"false")、remain(剩余毫秒)。
CCDelayTabButton
| 属性 | 值 | 说明 |
|---|---|---|
items | "监控,回放,设置" | 页签项列表 |
selected | int | 选中索引,默认 0 |
orientation | horizontal / vertical | 排布方向 |
gap | int | 项间距,夹到 0–40,默认 4 |
selectcolor | color | 选中底色,默认 #FF2F80C8 |
tabcolor | color | 普通底色,默认 #FF232A31 |
hotcolor | color | 悬停底色,默认 #FF2E3740 |
textcolor | color | 普通文字色,默认 #FFC8D4E0 |
selecttextcolor | color | 选中文字色,默认 #FFFFFFFF |
indicator | bool | 选中项下方高亮条,默认 true |
只读:selected、selectedtext、itemcount。
CCDelayBtnLayout
| 属性 | 值 | 说明 |
|---|---|---|
orientation | horizontal / vertical | 等分线方向 |
gap | int | 间隙值(当前只存不用) |
divider | bool | 画外框,默认 false |
dividercolor | color | 分隔线色,默认 #FF39424C |
cellcount | int | 等分格数,夹到 0–64,默认 0 |
只读:orientation、gap。
gap 属性在 CCDelayBtnLayout 里只存不用——SetGap 存了值,
但 PaintCNContent 里完全没用它。因为"不接管子控件布局",间隙由子控件
自己的 margin 控制。
五、XML 示例
<!-- 延时复位按钮:0.8 秒后触发 -->
<DelayResetBtn text="重置" delay="800" width="100" height="34" />
<!-- 倒计时中不允许重复点击 -->
<DelayResetBtn text="即刻重置" delay="2000" allowrepeat="false"
countcolor="#FF6B2A1E" width="120" height="34" />
<!-- 关掉进度条,只显示倒计时文字 -->
<DelayResetBtn text="延时启动" delay="3000" progress="false"
width="110" height="34" />
<!-- 三页签 -->
<DelayTabChangeButton items="主界面,日志,设置" selected="0"
gap="4" indicator="true"
width="300" height="36" />
<!-- 竖排页签 -->
<DelayTabChangeButton items="通道1,通道2,通道3" orientation="vertical"
width="100" height="120" />
<!-- 排布容器:3 等分辅助线 + 外框 -->
<SDelayResetBtnLayout orientation="horizontal" cellcount="3" divider="true"
height="44">
<DelayResetBtn text="复位A" delay="800" width="100" height="34" margin="0,0,8,0" />
<DelayResetBtn text="复位B" delay="800" width="100" height="34" margin="0,0,8,0" />
<DelayResetBtn text="复位C" delay="800" width="100" height="34" />
</SDelayResetBtnLayout>
显示效果
六、踩过的坑
6.1 页签需要 UIEVENT_MOUSEMOVE
见 2.5。MOUSEENTER 只能知道"在控件内",MOUSEMOVE 才能知道"在第几项上"。
当前靠 UIFLAG_TABSTOP 默认投递能工作,但建议显式加 UIFLAG_SETCURSOR。
6.2 CCDelayBtnLayout 的 gap 只存不用
见第四节。间隙由子控件 margin 控制。
6.3 items 保留空项
m_items.push_back(sItem); // 空项也推
"监控,,设置" → 3 项,中间空白。这是"保留占位"的刻意设计。
对比 FiveColorChart::labels 里是 if (!sItem.IsEmpty())——空项跳过。
两个控件的策略不同,因为页签需要"空位"占住横向空间,而状态条不需要。
6.4 倒计时进度条方向相反
见 1.5。延时按钮从满到空,普通按钮从空到满。别搞混。
6.5 SetSelected 与点击的通知策略不同
见 2.9。编程式设置无条件发,点击已选中项不发。
七、空状态处理
| 控件 | 空状态 | 行为 |
|---|---|---|
DelayResetBtn | text="" | 只有色块和进度条,无文字 |
DelayResetBtn | delay=0 | 按下后 m_dRemain=0,下一帧 OnTick 立即触发 delaydone |
DelayTabButton | items="" | m_items 为空 → PaintCNContent 首句返回,完全空白 |
DelayTabButton | items=",,," | 4 个空白页签(占位) |
DelayBtnLayout | cellcount=0 + divider=false | 完全不画(主动返回) |
DelayBtnLayout | cellcount=1 | i < 1 循环不执行,只可能画外框 |
转载自 CSDN-专业IT技术社区



