【SkinUI实例】仿QQ界面设计第三十课

文章转自:http://www.gymsaga.com/project/593.html

今天我们在来继续设计QQ界面,本节添加QQ窗口桌面自动隐藏功能和换肤功能,同时修复了之前的一些小bug

先说一下之前遗留下的小bug

1:搜索的Edit控件失去焦点后显示异常

解决方案:当Edit调用ShowWindow显示的时候,同时调用SetFocus,将当前的焦点权给Edit即可,这样当Edit失去焦点后,显示也就正常了,同时,为了达到QQ点击主面板任意位置也能恢复之前的效果,在窗口点击的WM_LBUTTONDOWN消息内,添加SetFocus();

2:QQ签名增加了编辑功能,原理和搜索的Edit处理一样,当点击QQ签名的按钮的时候,隐藏掉按钮,同时显示Edit,当Edit失去焦点后,将Edit隐藏,同时获取Edit的文本,将获取到的字符串显示在按钮上,参照代码的OnEnKillfocusEditPass方法

3:好友列表头像变灰,图像变灰代码如下

bool CImageEx::SetGray()
{
    //加载判断
    ASSERT(m_pImage!=NULL);
    if (m_pImage==NULL) return false;
   
    UINT nWidth = GetWidth();
    UINT nHeight = GetHeight();
   
    BitmapData bmpData;
    Rect rect(0, 0, nWidth, nHeight);
    m_pImage->LockBits(&rect, Gdiplus::ImageLockModeRead|Gdiplus::ImageLockModeWrite, PixelFormat32bppARGB, &bmpData);
   
    int nValue = 0;
    BYTE * pArray = (BYTE *)bmpData.Scan0;
    UINT nBytes = (UINT)((nWidth * nHeight) << 2);
   
    for (UINT i = 0; i < nBytes; i += 4)
    {
        nValue = (int)(pArray[i] * 0.1 + pArray[i+1] * 0.2 + pArray[i+2] * 0.7);
   
        pArray[i] = nValue;
        pArray[i+1] = nValue;
        pArray[i+2] = nValue;
    }
   
    m_pImage->UnlockBits(&bmpData);
   
    return true;
}

注意一点,这里是将图像m_pImage设置成了灰度图像,如果你想单纯的只是在绘制的时候成灰度的模样,可以使用ColorMatrix颜色矩阵将图片渲染成灰色


4:添加了QQ好友消息头像闪动的功能

m_BuddyList.SetBuddyItemHeadFlashAnim(nTeamIndex,0,TRUE);

在说一下QQ主面板的桌面自动隐藏功能:

先说一下原理,当按住QQ主面板放在桌面大约相差20像素的位置,如果是左边或者右边,窗口会进行自动拉伸,如果是上面或者是下面,窗口不会拉伸大小,当鼠标离开窗口大约200毫秒,窗口会自动移动到窗口一边,同时在桌面的边缘留出大约2像素的边框,这个边框正是用来捕捉鼠标的,当窗口捕获到鼠标消息时,自动进行移动,显示窗口。

先看一下代码

#pragma once
    
//窗口收缩类型
enum AFX_HIDE_TYPE
{
    en_None = 0,        //不收缩
    en_Top,             //向上收缩
    en_Bottom,          //向下收缩
    en_Left,            //向左收缩
    en_Right            //向右收缩
};
    
//////////////////////////////////////////////////////////////////////////
    
class CHideWindow
{
protected:
    bool                    m_bSized;                   //窗口大小是否改变
    bool                    m_bTimed;                   //是否设置了检测鼠标的Timer
    
    INT                     m_nWindowHeight;            //旧的窗口宽度
    INT                     m_nTaskBarHeight;           //任务栏高度
    INT                     m_nEdgeHeight;              //边缘高度
    INT                     m_nEdgeWidth;               //边缘宽度
    
    AFX_HIDE_TYPE           m_enHideType;               //隐藏模式
    bool                    m_bFinished;                //隐藏或显示过程是否完成
    bool                    m_bHiding;                  //该参数只有在!m_hsFinished才有效,真:正在隐藏,假:正在显示
    
    HWND                    m_hOwnHwnd;
    
public:
    CHideWindow(void);
    ~CHideWindow(void);
    
    //设置函数
public:
    //设置窗口属性
    void SetHideWindow(HWND hWnd,int nEdgeHeight=3,int nEdgeWidth=3);
    //修正移动时窗口的大小
    void FixMoving(UINT fwSide, LPRECT pRect);
    //修正改改变窗口大小时窗口的大小
    void FixSizing(UINT fwSide, LPRECT pRect);
    //从收缩状态显示窗口
    void ShowWindow();
    //从显示状态收缩窗口
    void HideWindow();
    //计时器消息
    void HideLoop( UINT nIDEvent );
    //开始隐藏
    void BeginHide(CPoint point);
    //重载函数
    BOOL SetWindowPos(HWND hWndInsertAfter,LPCRECT pCRect, UINT nFlags = SWP_SHOWWINDOW);
};

我们定义一个CHideWindow类,定义AFX_HIDE_TYPE枚举刚才鼠标拖动到桌面的四种位置,当我们按住窗口进行拖动时,我们响应WM_NCHITTEST消息,然后开启计时器,计时器的作用主要就是检测当前鼠标在窗口的位置,如果鼠标不在窗口上,就让他自动移动,隐藏起来,其次,我们需要窗口的WM_MOVING,通过这个消息,我们监听鼠标拖动窗口在桌面的位置,满足条件时,之前的计时器就会判断鼠标是否离开窗口,离开自然就启动第二个计时器(IDI_BEGINHIDE),实现慢慢移动的效果。到这里,自动拉伸隐藏就实现了,那么在说一下已经隐藏的窗口,我们怎么再让他显示出来的

void CHideWindow::HideLoop( UINT nIDEvent )
{
    if(nIDEvent == IDI_CHECKMOUSE )
    {
        CPoint point;
        GetCursorPos(&point);
    
        CRect rcWindow;
        ::GetWindowRect(m_hOwnHwnd,rcWindow);
            
        rcWindow.InflateRect(INFALTE,INFALTE);
    
        if(!rcWindow.PtInRect(point))
        {
            ::KillTimer(m_hOwnHwnd,IDI_CHECKMOUSE);
            m_bTimed = false;
    
            m_bFinished = false;           
            m_bHiding = true;
            ::SetTimer(m_hOwnHwnd,IDI_BEGINHIDE,BH_ELAPSE,NULL);
        }
        ::InvalidateRect(m_hOwnHwnd,NULL,FALSE);
    }
    else if(nIDEvent == IDI_BEGINHIDE)
    {
        if(m_bFinished)
            ::KillTimer(m_hOwnHwnd,IDI_BEGINHIDE);
        else
            m_bHiding ? HideWindow() : ShowWindow();   
    
        ::InvalidateRect(m_hOwnHwnd,NULL,FALSE);
    }
}

在看一下WM_NCHITTEST消息代码

void CHideWindow::BeginHide(CPoint point)
{
    if( (m_enHideType != en_None) && !m_bTimed && (point.x < GetSystemMetrics(SM_CXSCREEN) + INFALTE))
    {  
        ::SetTimer(m_hOwnHwnd,IDI_CHECKMOUSE,CM_ELAPSE,NULL);
        m_bTimed = true;
    
        m_bFinished = false;
        m_bHiding = false;
        ::SetTimer(m_hOwnHwnd,IDI_BEGINHIDE,BH_ELAPSE,NULL); //开启显示过程
    }
}

如上,我们在WM_NCHITTEST消息中同时将两个计时器都打开,刚才我们也说了,IDI_BEGINHIDE主要是实现窗口移动的动画,我们只需要通过变量m_bHiding,根据计时器IDI_CHECKMOUSE的鼠标位置判断去设置m_bHiding的值,那么当逻辑走到计时器IDI_BEGINHIDE的时候,隐藏和显示的效果也就实现了


在说一下换肤,

先说一下界面的做法,下方显示的图片列表可不是什么控件,在这里也提醒了大家,有时候我们做东西的时候不一定非要纠结于用什么控件,当遇到控件不能实现的效果,我们完全可以去模拟出来,好,言归正传,我们先观察一下QQ的换肤,打开皮肤窗口,他会从网站下载最新皮肤数据,放在了我的账户的用户文件夹中,同时每个皮肤会附带一个xml配置文件,代码如下

<?xml version="1.0" encoding="utf-8" ?>
<theme-config PackID="C148CE621F870A05B54AC459DDEAA5B0" PackPreview="preview.png" PackLogonPreview="logon_preview.png" PackRecentPreview="recent_preview.png" PackVersion="2.0" PackMinVer="2509" PackName="skin">
    <backgroundlist>
        <background preview="bg_preview.png" mainfile="main.png" mainloc="topleft" aioloc="topleft" bkgstyle="stretch" color="ff1e7db9" />
    </backgroundlist>
</theme-config>

从代码中我们发现,里面有加载的图片名称,绘制图像的位置,绘制模式,可能最后的color你会发愣,这个是用来做什么的呢,当我们拉伸窗口大小时,如果窗口大于图片的尺寸,他会用这个color填充不能显示的区域,说白了也就是用color填充整个客户区,然后用绘制了该背景图片,代码中,我没有像QQ那样从配置文件说读取这些属性,当然你可以实现这个功能,其次,我并没有用color去填充背景,而是选择图片的一列像素进行的拉伸处理,其次,代码中最后三个皮肤,采用的是topleft绘制方式,上面的12中是righttop方式,绘制的方法详见代码吧。其次说一下重点,当我们打开这个皮肤设置窗口还有QQ会话聊天窗口,绘画的都是同一个模样,那么我们怎么将这些窗口关联起来的呢?

我们创建这些窗口的基类对话框CSkinManager,通过CSkinManager,我们派生出这些一个模样的窗口,然后创建一个vector,我们将所有窗口的窗口指针全部压到该vcetor中,创建vector的作用正是在换肤的时候,我们可以及时的通知所有窗口去更新背景,不过这里需要注意一点,我们之前将窗口的Clip Children设为了true如果只是单纯的调用窗口的Invalidate函数的话,背景是换掉了,但是由于剪辑掉了控件的位置,所以导致控件的旧背景仍然存在,解决办法相当接单,当我们点击换肤时,将Clip Children属性先设为false,刷新完界面后,我们在将Clip Children属性设为true即可,代码如下

void CSkinManager::UpdateSkin()
{
    RenderEngine->RemoveImage(m_pImageBack);
    m_pImageBack = RenderEngine->GetImage(GlobalUnits->m_szDefaultSkin);
   
    for (int i=0;i<GlobalUnits->m_WindowArray.size();i++)
    {
        CSkinManager * pSkinManager = GlobalUnits->m_WindowArray.at(i);
   
        if (pSkinManager->GetSafeHwnd() == NULL ) continue;
           
        pSkinManager->ModifyStyle(WS_CLIPCHILDREN,0);
   
        pSkinManager->Invalidate(FALSE);
   
        pSkinManager->ModifyStyle(0,WS_CLIPCHILDREN);
    }
}

关于QQ会话聊天窗口的创建

添加QQ好友列表双击事件响应消息

ON_NOTIFY(NM_DBLCLK, IDC_BUDDYLIST, &CQQDlg::OnBnClickOpenChat)

关于窗口的创建,我们发现,假如打开了A窗口,我们在双击A的话,他只会将该窗口置前而不是重新又创建出这么一个窗口来,所以我们采用vector来管理聊天窗口

void CQQDlg::OnBnClickOpenChat( NMHDR *pNMHDR, LRESULT *pResult )
{
    int nSelTeamIndex = m_BuddyList.GetSelTeamIndex();
    int nSelIndex = m_BuddyList.GetSelIndex();
   
    if ( m_BuddyList.IsBuddyItemHasMsg(nSelTeamIndex,nSelIndex) )
    {
        m_BuddyList.SetBuddyItemHeadFlashAnim(nSelTeamIndex,nSelIndex,FALSE);
    }
   
    for (int i=0;i<GlobalUnits->m_ChatArray.size();i++)
    {
        CChatDlg * pChatDlg = GlobalUnits->m_ChatArray.at(i);
        if ( StrCmp(pChatDlg->m_pBuddyItem->m_strQQNum, m_BuddyList.GetBuddyItemQQNum(nSelTeamIndex,nSelIndex) ) == 0 )
        {
            pChatDlg->ShowWindow(SW_SHOW);
            pChatDlg->SetForegroundWindow();
   
            return;
        }
    }
   
    CChatDlg *pChatDlg = new CChatDlg;
    pChatDlg->Create(IDD_CHAT);
    pChatDlg->ShowWindow(SW_SHOW);
    pChatDlg->CenterWindow(GetDesktopWindow());
   
    pChatDlg->m_pBuddyItem = m_BuddyList.GetBuddyItemByIndex(nSelTeamIndex,nSelIndex);
       
    GlobalUnits->m_ChatArray.push_back(pChatDlg);
}

上面的代码,我们如果点击了闪动头像的聊天窗口,先取消闪动,其次,因为QQ号没有重复的,所以我们通过匹配QQ号的方式来查找是否存在当前窗口,当然方法还有很多,仅供参考。

明天我们继续完善QQ绘画聊天窗口

代码下载地址:http://pan.baidu.com/s/1quc4x






你可能感兴趣的:(【SkinUI实例】仿QQ界面设计第三十课)