Feature: Add CListCtrlEx with column show/hide support

- Create CListCtrlEx class derived from CListCtrl
- Support right-click header menu to toggle column visibility
- Save column visibility settings to registry (list\{ConfigKey})
- Settings persist by column name, not index (order-independent)
- Skip empty column titles in context menu
- Update CClientListDlg to use CListCtrlEx
- Update m_CList_Online in 2015RemoteDlg to use CListCtrlEx
This commit is contained in:
yuanyuanxiang
2026-01-26 17:57:03 +01:00
parent d7789e04ca
commit d49c541ea2
8 changed files with 367 additions and 154 deletions

View File

@@ -0,0 +1,270 @@
#include "stdafx.h"
#include "CListCtrlEx.h"
#include "2015Remote.h"
// CHeaderCtrlEx 实现
BEGIN_MESSAGE_MAP(CHeaderCtrlEx, CHeaderCtrl)
ON_NOTIFY_REFLECT(HDN_BEGINTRACKA, &CHeaderCtrlEx::OnBeginTrack)
ON_NOTIFY_REFLECT(HDN_BEGINTRACKW, &CHeaderCtrlEx::OnBeginTrack)
END_MESSAGE_MAP()
void CHeaderCtrlEx::OnBeginTrack(NMHDR* pNMHDR, LRESULT* pResult)
{
LPNMHEADER pNMHeader = reinterpret_cast<LPNMHEADER>(pNMHDR);
int nCol = pNMHeader->iItem;
if (m_pListCtrl && nCol >= 0 && nCol < (int)m_pListCtrl->m_Columns.size()) {
if (!m_pListCtrl->m_Columns[nCol].Visible) {
*pResult = TRUE; // 阻止拖拽
return;
}
}
*pResult = FALSE;
}
// CListCtrlEx 实现
IMPLEMENT_DYNAMIC(CListCtrlEx, CListCtrl)
CListCtrlEx::CListCtrlEx()
{
}
CListCtrlEx::~CListCtrlEx()
{
}
BEGIN_MESSAGE_MAP(CListCtrlEx, CListCtrl)
ON_WM_CONTEXTMENU()
END_MESSAGE_MAP()
void CListCtrlEx::SetConfigKey(const CString& strKey)
{
m_strConfigKey = strKey;
}
int CListCtrlEx::AddColumn(int nCol, LPCTSTR lpszColumnHeading, int nWidth, int nFormat, BOOL bCanHide)
{
// 添加到列表控件
int nResult = InsertColumn(nCol, lpszColumnHeading, nFormat, nWidth);
if (nResult != -1) {
// 保存列信息
ColumnInfoEx info;
info.Name = lpszColumnHeading;
info.Width = nWidth;
info.Percent = 0.0f; // 稍后在 InitColumns 中计算
info.Visible = TRUE;
info.CanHide = bCanHide;
// 确保 vector 大小足够
if (nCol >= (int)m_Columns.size()) {
m_Columns.resize(nCol + 1);
}
m_Columns[nCol] = info;
}
return nResult;
}
void CListCtrlEx::InitColumns()
{
if (m_Columns.empty()) {
return;
}
// 子类化 Header 控件
SubclassHeader();
// 计算总宽度和百分比
int totalWidth = 0;
for (const auto& col : m_Columns) {
totalWidth += col.Width;
}
if (totalWidth > 0) {
for (auto& col : m_Columns) {
col.Percent = (float)col.Width / totalWidth;
}
}
// 从配置加载列可见性
LoadColumnVisibility();
// 应用列宽
AdjustColumnWidths();
}
void CListCtrlEx::SubclassHeader()
{
CHeaderCtrl* pHeader = GetHeaderCtrl();
if (pHeader && !m_HeaderCtrl.GetSafeHwnd()) {
m_HeaderCtrl.SubclassWindow(pHeader->GetSafeHwnd());
m_HeaderCtrl.m_pListCtrl = this;
}
}
void CListCtrlEx::AdjustColumnWidths()
{
if (m_Columns.empty() || GetSafeHwnd() == NULL) {
return;
}
CRect rect;
GetClientRect(&rect);
int totalWidth = rect.Width() - 20; // 减去滚动条宽度
// 计算可见列的总百分比
float visiblePercent = 0.0f;
for (const auto& col : m_Columns) {
if (col.Visible) {
visiblePercent += col.Percent;
}
}
// 按比例分配宽度给可见列
for (int i = 0; i < (int)m_Columns.size(); i++) {
if (m_Columns[i].Visible) {
int width = (visiblePercent > 0) ? (int)(totalWidth * m_Columns[i].Percent / visiblePercent) : 0;
SetColumnWidth(i, width);
} else {
SetColumnWidth(i, 0); // 隐藏列
}
}
}
BOOL CListCtrlEx::IsColumnVisible(int nCol) const
{
if (nCol >= 0 && nCol < (int)m_Columns.size()) {
return m_Columns[nCol].Visible;
}
return TRUE;
}
void CListCtrlEx::SetColumnVisible(int nCol, BOOL bVisible)
{
if (nCol >= 0 && nCol < (int)m_Columns.size()) {
if (m_Columns[nCol].CanHide || bVisible) { // 不允许隐藏的列只能设为可见
m_Columns[nCol].Visible = bVisible;
AdjustColumnWidths();
}
}
}
void CListCtrlEx::OnContextMenu(CWnd* pWnd, CPoint pt)
{
// 检查是否点击在表头区域
CHeaderCtrl* pHeader = GetHeaderCtrl();
if (pHeader) {
CRect headerRect;
pHeader->GetWindowRect(&headerRect);
if (headerRect.PtInRect(pt)) {
ShowColumnContextMenu(pt);
return;
}
}
// 非表头区域,调用父类处理
CListCtrl::OnContextMenu(pWnd, pt);
}
void CListCtrlEx::ShowColumnContextMenu(CPoint pt)
{
CMenu menu;
menu.CreatePopupMenu();
// 添加所有列到菜单(跳过空标题的列)
for (int i = 0; i < (int)m_Columns.size(); i++) {
// 跳过空标题的列
if (m_Columns[i].Name.IsEmpty()) {
continue;
}
UINT flags = MF_STRING;
if (m_Columns[i].Visible) {
flags |= MF_CHECKED;
}
// 不允许隐藏的列显示为灰色
if (!m_Columns[i].CanHide) {
flags |= MF_GRAYED;
}
menu.AppendMenu(flags, 1000 + i, m_Columns[i].Name);
}
// 显示菜单并获取选择
int nCmd = menu.TrackPopupMenu(TPM_RETURNCMD | TPM_LEFTALIGN | TPM_RIGHTBUTTON, pt.x, pt.y, this);
if (nCmd >= 1000 && nCmd < 1000 + (int)m_Columns.size()) {
ToggleColumnVisibility(nCmd - 1000);
}
}
void CListCtrlEx::ToggleColumnVisibility(int nColumn)
{
if (nColumn < 0 || nColumn >= (int)m_Columns.size()) {
return;
}
// 不允许隐藏的列不处理
if (!m_Columns[nColumn].CanHide) {
return;
}
// 切换可见性
m_Columns[nColumn].Visible = !m_Columns[nColumn].Visible;
// 保存到配置
SaveColumnVisibility();
// 重新调整列宽
AdjustColumnWidths();
}
void CListCtrlEx::LoadColumnVisibility()
{
if (m_strConfigKey.IsEmpty()) {
return; // 没有设置配置键,不加载
}
// 配置结构list\{ConfigKey},如 list\ClientList
// 格式:逗号分隔的隐藏列名,如 "备注,程序路径"
CT2A configKeyA(m_strConfigKey);
std::string strHidden = THIS_CFG.GetStr("list", std::string(configKeyA), "");
if (strHidden.empty()) {
return; // 使用默认值(全部显示)
}
// 解析隐藏的列名
std::vector<std::string> hiddenNames = StringToVector(strHidden, ',');
for (const auto& name : hiddenNames) {
// 查找列名对应的索引
for (int i = 0; i < (int)m_Columns.size(); i++) {
CT2A colNameA(m_Columns[i].Name);
if (name == std::string(colNameA)) {
if (m_Columns[i].CanHide) {
m_Columns[i].Visible = FALSE;
}
break;
}
}
}
}
void CListCtrlEx::SaveColumnVisibility()
{
if (m_strConfigKey.IsEmpty()) {
return; // 没有设置配置键,不保存
}
// 只保存隐藏的列名
std::string strHidden;
for (int i = 0; i < (int)m_Columns.size(); i++) {
if (!m_Columns[i].Visible) {
if (!strHidden.empty()) strHidden += ",";
CT2A colNameA(m_Columns[i].Name);
strHidden += std::string(colNameA);
}
}
// 配置结构list\{ConfigKey},如 list\ClientList
CT2A configKeyA(m_strConfigKey);
THIS_CFG.SetStr("list", std::string(configKeyA), strHidden);
}