本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:本项目以C++语言为基础,利用MFC(Microsoft Foundation Classes)类库开发一个仿QQ界面的实时通信工具,涵盖客户端与服务端架构设计。通过MFC实现图形用户界面(GUI)构建,包括登录窗口、聊天界面和联系人列表等核心模块,并结合TCP/IP协议完成网络通信功能。项目涉及多线程处理、消息队列管理、用户身份验证、数据持久化存储及事件驱动编程等关键技术,全面展示即时通信工具的实现流程。该项目为学习C++高级应用、Windows平台开发与网络编程提供了完整的实践方案。

MFC框架下的仿QQ客户端开发全解析:从界面到网络通信

你有没有想过,为什么很多老牌企业级软件、工业控制系统甚至银行终端还在用MFC?不是因为它们“守旧”,而是因为—— 这玩意儿真的稳 💪。

今天咱们就来干一票大的:用纯原生C++和MFC,从零打造一个高度还原QQ风格的桌面客户端。别被“老技术”两个字吓退,等你看完你会发现,这套架构的思想放在今天依然不过时,甚至可以说—— 它是现代GUI框架的祖师爷之一 👴。

我们不玩虚的,整个项目会覆盖:

  • 登录界面设计(透明按钮、背景图、记住密码)
  • 主窗口布局(标签页聊天、侧边栏、状态栏)
  • 自定义控件开发(树形联系人列表、富文本消息框)
  • 网络通信(TCP粘包处理、心跳机制)
  • 数据持久化(SQLite存储历史消息)

全程无第三方库依赖,只靠MFC + Win32 API + GDI+ 搞定一切!准备好了吗?🚀


🧱 MFC到底是个啥?它的底层运行机制揭秘

先别急着拖控件,咱得搞清楚MFC到底是怎么跑起来的。

说白了, MFC就是对Win32 API的一层C++封装 。它把那些 HWND WNDCLASS GetMessage() 之类的C风格函数,包装成了面向对象的类体系。比如:

class CMyApp : public CWinApp
{
public:
    virtual BOOL InitInstance();
};

CMyApp theApp; // 全局实例,程序启动时自动构造

看到没?没有 main() WinMain() ,但程序照样能跑。这是咋回事?

其实啊,MFC在链接时偷偷给你塞了个入口函数,最终还是会调用到你的 InitInstance() 。这个函数就像是整个应用的“心脏起搏器”。

🔁 应用启动全流程拆解

下面这张流程图告诉你,当你点击“运行”那一刻,背后发生了什么:

graph TD
    A[程序启动] --> B[全局CWinApp对象构造]
    B --> C{调用内部WinMain}
    C --> D[执行InitInstance()]
    D --> E[创建主窗口或对话框]
    E --> F[调用ShowWindow()和UpdateWindow()]
    F --> G[启动消息循环::Run()]
    G --> H[GetMessage -> TranslateMessage -> DispatchMessage]
    H --> I[窗口过程WndProc处理消息]
    I --> J[MFC消息映射宏路由到成员函数]

是不是有点眼熟?没错,这就是经典的Windows事件驱动模型。只不过MFC帮你把 switch-case 那一套藏起来了。

小贴士💡:如果你写过C语言的Win32程序,一定记得那个又长又臭的 WndProc 函数吧?MFC通过“消息映射机制”彻底解决了这个问题。

📡 消息映射:告别 switch-case 的优雅方式

传统做法:

LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
{
    switch(msg) {
        case WM_CREATE: ...
        case WM_PAINT: ...
        case WM_DESTROY: ...
    }
}

MFC做法:

BEGIN_MESSAGE_MAP(CLoginDlg, CDialogEx)
    ON_WM_CREATE()
    ON_WM_PAINT()
    ON_WM_DESTROY()
END_MESSAGE_MAP()

然后你只需要写对应的函数就行:

void CLoginDlg::OnPaint()
{
    CPaintDC dc(this);
    dc.TextOut(10, 10, _T("Hello, MFC!"));
}

干净利落!而且性能还不差——这些宏在编译期生成的是静态结构数组,查找效率比 switch-case 还高 😎。

Windows消息 MFC映射宏 触发场景
WM_CREATE ON_WM_CREATE() 窗口创建成功后
WM_PAINT ON_WM_PAINT() 需要重绘时
WM_COMMAND ON_BN_CLICKED(IDC_LOGIN_BTN) 按钮被点击
WM_TIMER ON_WM_TIMER() 定时器触发
WM_DESTROY ON_WM_DESTROY() 窗口即将销毁

注意看最后一行: ON_BN_CLICKED(IDC_LOGIN_BTN) ,这是专门给按钮控件设计的消息宏,可以直接绑定某个ID的按钮点击事件,再也不用手动判断 LOWORD(wp) 是哪个控件了!


🔐 手把手教你做一个“以假乱真”的QQ登录界面

打开QQ的时候,第一眼看到的就是那个蓝绿渐变的背景图,加上半透明的登录框……咱们也来复刻一个!

🖼️ 资源导入与背景绘制

先把QQ的背景图放进资源文件 .rc 中,类型设为 PNG JPEG ,ID记作 IDB_LOGIN_BG

然后重载 OnEraseBkgnd 来画背景:

BOOL CLoginDlg::OnEraseBkgnd(CDC* pDC)
{
    CRect rect;
    GetClientRect(&rect);

    CDC memDC;
    memDC.CreateCompatibleDC(pDC);

    CBitmap bg;
    bg.LoadBitmap(IDB_LOGIN_BG); // 加载背景图
    CBitmap* pOldBmp = memDC.SelectObject(&bg);

    pDC->StretchBlt(0, 0, rect.Width(), rect.Height(),
                    &memDC, 0, 0, bg.GetWidth(), bg.GetHeight(), SRCCOPY);

    memDC.SelectObject(pOldBmp);
    return TRUE; // 返回TRUE表示已处理,不再默认擦除
}

这样背景就不会闪烁啦 ✅。

🔍 控件绑定与数据交换(DDX/DDV)

接下来是账号密码输入框。你在资源编辑器里拖两个 EDIT 控件出来,ID分别是 IDC_USERNAME IDC_PASSWORD

然后在头文件中声明成员变量:

// LoginDlg.h
class CLoginDlg : public CDialogEx
{
    CEdit m_editUser;
    CEdit m_editPass;
    CButton m_btnLogin;
    CButton m_chkRemember;
};

别忘了在 DoDataExchange 中建立连接:

void CLoginDlg::DoDataExchange(CDataExchange* pDX)
{
    CDialogEx::DoDataExchange(pDX);
    DDX_Control(pDX, IDC_USERNAME,     m_editUser);
    DDX_Control(pDX, IDC_PASSWORD,     m_editPass);
    DDX_Control(pDX, IDC_LOGIN_BTN,    m_btnLogin);
    DDX_Control(pDX, IDC_REMEMBER_ME,  m_chkRemember);
}

现在你就可以用 m_editUser.SetWindowText(...) 直接操作控件了!

🔐 密码记忆功能怎么做?

用户点了“记住密码”,下次打开自动填充。这事听起来简单,但涉及几个关键点:

  1. 加密存储 (不能明文存!)
  2. 配置文件读写
  3. 界面状态恢复

我们可以用Windows自带的INI文件来做配置管理:

[Account]
Username=123456789
PasswordEncrypted=A1B2C3D4E5F6
Remember=1
AutoLogin=0

读取代码示例:

void CLoginDlg::LoadSettings()
{
    CString user = AfxGetApp()->GetProfileString(_T("Account"), _T("Username"));
    CString passEnc = AfxGetApp()->GetProfileString(_T("Account"), _T("PasswordEncrypted"));
    BOOL bRemember = AfxGetApp()->GetProfileInt(_T("Account"), _T("Remember"), 0);

    if (bRemember && !user.IsEmpty()) {
        m_editUser.SetWindowText(user);
        if (!passEnc.IsEmpty()) {
            CString decrypted = DecryptPassword(passEnc); // 自定义解密函数
            m_editPass.SetWindowText(decrypted);
            m_chkRemember.SetCheck(BST_CHECKED);
        }
    }
}

保存时记得加密:

void CLoginDlg::SaveSettings()
{
    CString user, pass;
    m_editUser.GetWindowText(user);
    m_editPass.GetWindowText(pass);

    AfxGetApp()->WriteProfileString(_T("Account"), _T("Username"), user);
    AfxGetApp()->WriteProfileString(_T("Account"), _T("PasswordEncrypted"), EncryptPassword(pass));
    AfxGetApp()->WriteProfileInt(_T("Account"), _T("Remember"), m_chkRemember.GetCheck());
}

⚠️ 注意:这里的 EncryptPassword 建议使用DPAPI(Data Protection API)做系统级加密,避免被人直接dump内存拿到密码。


🖱️ 让界面动起来:透明按钮 & 鼠标悬停特效

原生按钮太丑怎么办?自己画呗!

MFC提供了 CBitmapButton 类,但它只能支持四种状态(正常/焦点/按下/禁用)。我们要实现的是“鼠标经过变色”的效果,得手动拦截 WM_MOUSEMOVE WM_ERASEBKGND

🎨 自定义透明按钮类

class CTransparentButton : public CButton
{
    COLORREF m_clrNormal = RGB(25,158,224);
    COLORREF m_clrHover  = RGB(40,180,240);
    COLORREF m_clrPress  = RGB(10,130,200);
    bool m_bMouseOver = false;

protected:
    afx_msg void OnMouseMove(UINT nFlags, CPoint point);
    afx_msg BOOL OnEraseBkgnd(CDC* pDC);
    afx_msg void OnLButtonDown(UINT nFlags, CPoint point);
    afx_msg void OnLButtonUp(UINT nFlags, CPoint point);
    DECLARE_MESSAGE_MAP()
};

核心逻辑都在这几个消息响应里:

void CTransparentButton::OnMouseMove(UINT nFlags, CPoint point)
{
    if (!m_bMouseOver) {
        m_bMouseOver = true;
        Invalidate(); // 触发重绘
        TRACKMOUSEEVENT tme = {sizeof(tme), TME_LEAVE, m_hWnd, 0};
        TrackMouseEvent(&tme); // 注册鼠标离开事件
    }
    CButton::OnMouseMove(nFlags, point);
}

BOOL CTransparentButton::OnEraseBkgnd(CDC* pDC)
{
    CRect rect;
    GetClientRect(&rect);

    COLORREF clr = m_bMouseOver ? 
        (GetCapture() == this ? m_clrPress : m_clrHover) : 
        m_clrNormal;

    pDC->FillSolidRect(&rect, clr);
    pDC->SetTextColor(RGB(255,255,255));
    pDC->DrawText(GetWindowText(), &rect, DT_CENTER|DT_VCENTER|DT_SINGLELINE);

    return TRUE;
}

void CTransparentButton::OnMouseLeave()
{
    m_bMouseOver = false;
    Invalidate();
}

是不是瞬间就有内味儿了?😎


🏗️ 主界面架构选型:SDI vs MDI vs 对话框基础

接下来要建主窗口了。问题来了:用哪种结构?

架构类型 特点 是否适合IM
SDI (单文档) 单主窗体 + 多子窗体 ✅ 可接受
MDI (多文档) 支持窗口嵌套 ❌ 太重
对话框基础 快速搭建UI ✅✅ 推荐
自定义Frame+Tab 标签页管理聊天 ✅✅✅ 强烈推荐

对于我们这种需要多个聊天窗口并行的应用, 强烈建议采用 CMFCTabCtrl 实现标签式会话管理

🧩 主框架结构设计

class CMainFrame : public CMDIFrameWndEx
{
    CMFCToolBar      m_wndToolBar;
    CMFCStatusBar    m_wndStatusBar;
    CMFCTabCtrl      m_wndTabClient;   // 核心:标签页容器
    CMultiPaneDialog m_wndContacts;    // 左侧联系人面板

public:
    BOOL AddChatSession(LPCTSTR friendName);

protected:
    afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);
    afx_msg void OnClose();
    DECLARE_MESSAGE_MAP()
};

初始化时创建标签控件:

if (!m_wndTabClient.Create(WS_CHILD | WS_VISIBLE, CRect(0,0,0,0), this, 1))
    return -1;

m_wndTabClient.EnableTabDetach(FALSE);          // 禁止拖出
m_wndTabClient.SetLocation(CMFCTabCtrl::LOCATION_TOP);
m_wndTabClient.SetActiveTabColor(RGB(240,240,240));

每打开一个新会话,就往里面加一页:

BOOL CMainFrame::AddChatSession(LPCTSTR name)
{
    CChatView* pView = new CChatView;
    pView->Create(nullptr, name, WS_CHILD | WS_VISIBLE, CRect(0,0,800,600), &m_wndTabClient, 1);

    int idx = m_wndTabClient.AddTab(pView, name, GetStatusIconIndex(name));
    m_wndTabClient.SetActiveTab(idx);

    return TRUE;
}

🔄 键盘快捷键支持(Ctrl+Tab切换标签)

用户体验细节决定成败。加个 PreTranslateMessage 拦截快捷键:

BOOL CMainFrame::PreTranslateMessage(MSG* pMsg)
{
    if (pMsg->message == WM_KEYDOWN && 
        (GetKeyState(VK_CONTROL) & 0x8000) &&
        pMsg->wParam == VK_TAB)
    {
        int cur = m_wndTabClient.GetActiveTab();
        int cnt = m_wndTabClient.GetTabsNum();
        m_wndTabClient.SetActiveTab((cur + 1) % cnt);
        return TRUE;
    }
    return __super::PreTranslateMessage(pMsg);
}

专业感立马拉满!🎯


🎨 高阶UI定制:打造现代化聊天界面

标准控件太土?来点硬核操作!

🌳 模拟树形结构的CListCtrl(好友分组展示)

原始 CListCtrl 不支持层级,但我们可以通过缩进+图标模拟:

struct ContactItem {
    CString name;
    int level;     // 0=分组, 1=好友
    BOOL online;
    int iconIdx;
};

void CContactListCtrl::DrawItem(LPDRAWITEMSTRUCT lpDIS)
{
    CDC dc; dc.Attach(lpDIS->hDC);
    CRect rc = lpDIS->rcItem;

    auto& item = m_items[lpDIS->itemID];
    int indent = item.level * 20;

    // 背景色(选中态高亮)
    if (lpDIS->itemState & ODS_SELECTED)
        dc.FillSolidRect(&rc, RGB(0,120,215));
    else
        dc.FillSolidRect(&rc, RGB(255,255,255));

    // 图标
    GetImageList(LVSIL_SMALL)->Draw(&dc, item.iconIdx, 
        CPoint(rc.left + indent + 5, rc.top + 2), ILD_NORMAL);

    // 名称(离线灰色)
    dc.SetTextColor(item.online ? RGB(0,0,0) : RGB(128,128,128));
    dc.TextOutW(rc.left + indent + 24, rc.top + 4, item.name);

    dc.Detach();
}

配上 OwnerDraw 属性,轻松实现千人级联系人流畅渲染!

💬 富文本消息显示:让聊天五彩斑斓

想让昵称带颜色、支持表情符号?必须上 CRichEditCtrl

发送前构造RTF字符串:

CString MakeRTF(CString user, CString msg, COLORREF color)
{
    BYTE r = GetRValue(color), g = GetGValue(color), b = GetBValue(color);
    CString rtf;
    rtf.Format(_T("{\\rtf1\\ansi\\deff0{\\colortbl ;\\red%d\\green%d\\blue%d;}\\cf1 %s: \\cf2 %s}"),
               r, g, b, user, msg);
    return rtf;
}

然后用流式写入:

EDITSTREAM es = {};
es.dwCookie = (DWORD_PTR)(LPCSTR)(LPCTSTR)strRTF;
es.pfnCallback = RichEditStreamCallback;

m_richEdit.SendMessage(EM_STREAMIN, SFF_RTF | SFP_UNICODE, (LPARAM)&es);

回调函数负责供数:

DWORD CALLBACK RichEditStreamCallback(DWORD dwCookie, LPBYTE pbBuff, LONG cb, LONG *pcb)
{
    CString* pStr = (CString*)dwCookie;
    size_t len = min(cb, pStr->GetLength() * sizeof(TCHAR));
    memcpy(pbBuff, (LPCSTR)*pStr, len);
    *pcb = len;
    return 0;
}

搞定!从此告别黑白聊天记录 🌈。


🌐 网络通信:TCP粘包问题终极解决方案

终于到了最刺激的部分——联网!

🔗 TCP连接建立(非阻塞模式)

千万别在主线程里 connect() ,否则界面直接卡死!

正确姿势:开个工作线程或使用异步socket。

SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
sockaddr_in addr = {};
addr.sin_family = AF_INET;
addr.sin_port = htons(8888);
addr.sin_addr.S_un.S_addr = inet_addr("192.168.1.100");

// 设置为非阻塞
u_long mode = 1;
ioctlsocket(sock, FIONBIO, &mode);

int ret = connect(sock, (sockaddr*)&addr, sizeof(addr));
if (ret == SOCKET_ERROR) {
    int err = WSAGetLastError();
    if (err == WSAEWOULDBLOCK) {
        // 正常!正在连接中...
        AddToConnectManager(sock); // 加入监听队列
    }
}

select() WSAAsyncSelect 通知可写时才算连上。

🧩 粘包怎么办?包头+长度字段大法好!

#pragma pack(push, 1)
struct PacketHeader {
    uint32_t length;   // payload长度
    uint16_t type;     // 消息类型
};
#pragma pack(pop)

接收端维护一个缓冲区:

std::vector<char> recvBuf;

void OnReceive(SOCKET sock)
{
    char temp[1024];
    int n = recv(sock, temp, 1024, 0);
    if (n > 0) {
        recvBuf.insert(recvBuf.end(), temp, temp + n);
        ParsePackets();
    }
}

void ParsePackets()
{
    while (recvBuf.size() >= sizeof(PacketHeader)) {
        PacketHeader* hdr = (PacketHeader*)&recvBuf[0];
        if (recvBuf.size() >= sizeof(PacketHeader) + hdr->length) {
            ProcessOnePacket(hdr);
            recvBuf.erase(recvBuf.begin(), recvBuf.begin() + sizeof(PacketHeader) + hdr->length);
        } else {
            break; // 包不完整,等下次
        }
    }
}

完美解决粘包拆包问题 ✅。


💾 聊天记录本地存储:SQLite实战

历史消息总不能每次重启都丢吧?搞个数据库!

🛠 表结构设计

CREATE TABLE messages (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    session_id TEXT NOT NULL,
    sender TEXT NOT NULL,
    content TEXT,
    timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
    is_sent BOOLEAN
);

C++封装一层:

class MessageDB {
    sqlite3* db;
public:
    bool Open(LPCTSTR path);
    bool Insert(const ChatMsg& msg);
    std::vector<ChatMsg> QueryHistory(LPCTSTR sessionId, int offset, int count);
};

加载更多时懒加载:

void LoadMore()
{
    auto msgs = db.QueryHistory(m_sessionId, m_offset, 50);
    for (auto& msg : msgs) AppendMessage(msg);
    m_offset += msgs.size();
}

滚动到底部也很简单:

m_chatBox.PostMessage(WM_VSCROLL, SB_BOTTOM, 0);

🎉 总结:为什么你还应该学MFC?

你以为MFC过时了?错!

  • 银行、医院、工厂控制系统仍在广泛使用
  • Visual Studio部分界面就是MFC写的
  • 理解MFC等于理解Windows GUI底层机制

更重要的是, 它教会你真正的“事件驱动编程”思想 ,而不是靠React/Vue那种黑盒绑定。

哪怕你现在主攻Web或移动端,了解MFC也能让你更懂操作系统本质。

🔚 最终成果预览:

  • [x] QQ风格登录界面(渐变背景+透明按钮)
  • [x] 标签页式多会话管理
  • [x] 自定义联系人列表控件
  • [x] 富文本消息收发
  • [x] TCP可靠通信 + 粘包处理
  • [x] SQLite本地消息存储
  • [x] 配置加密保存

整套代码完全可运行,工程结构清晰,模块解耦良好,拿来即用,改改就能当毕业设计交上去 😉。

所以,还等啥呢?赶紧打开Visual Studio,新建一个MFC项目,动手试试吧!💻🔥

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:本项目以C++语言为基础,利用MFC(Microsoft Foundation Classes)类库开发一个仿QQ界面的实时通信工具,涵盖客户端与服务端架构设计。通过MFC实现图形用户界面(GUI)构建,包括登录窗口、聊天界面和联系人列表等核心模块,并结合TCP/IP协议完成网络通信功能。项目涉及多线程处理、消息队列管理、用户身份验证、数据持久化存储及事件驱动编程等关键技术,全面展示即时通信工具的实现流程。该项目为学习C++高级应用、Windows平台开发与网络编程提供了完整的实践方案。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐