Fork me on GitHub

Duilib的控件拖拽排序,支持跨容器拖拽(网易云信版本)

            

 

 

           

           完整代码见:https://github.com/netease-im/NIM_Duilib_Framework/pull/151

 

          核心代码(思路):

   appitem.h

#pragma once

#define APP_HEIGHT 90
#define APP_WIDTH  90
#define EACH_LINE  6

#include <string>
//app的具体信息,这里假定有id,name,_icon,_isFrequent自行拓展
struct AppItem
{
    std::string _id;
    std::wstring _name;
    std::wstring _icon;
    bool _isFrequent=false;
};


//App UI类
class AppItemUi : public ui::VBox
{
public:
    static AppItemUi* Create(const AppItem& item);
    virtual void DoInit();
    void SetAppdata(const AppItem& item,bool refresh);
    void FixPos(int step,int index=-1);   //前进/后退多少步  目前应该有-1 0 1    
    inline int getIndex() const { return index_; }
    inline const AppItem& getAppData() const { return app_data_; }
private:
    AppItem app_data_;
    int index_ = 0;    //第几个
    ui::Control* app_icon_ = nullptr;
    ui::Label* app_name_ = nullptr;
};


//AppWindow 拖动显示窗口类
//最好半透明
class AppWindow : public ui::WindowImplBase
{
public:
    AppWindow();
    ~AppWindow();

    static AppWindow* CreateAppWindow(HWND hParent, POINT pt, const AppItem& Item)
    {
        AppWindow* ret = new AppWindow;
        ret->SetBeforeCreate(Item, pt);
        ret->Create(hParent, L"", WS_POPUP, WS_EX_TOOLWINDOW);
        pThis_ = ret;
        //需要改变下pos,延后到initWindows
        return ret;
    }

    /**
    * 一下三个接口是必须要覆写的接口,父类会调用这三个接口来构建窗口
    * GetSkinFolder        接口设置你要绘制的窗口皮肤资源路径
    * GetSkinFile            接口设置你要绘制的窗口的 xml 描述文件
    * GetWindowClassName    接口设置窗口唯一的类名称
    */
    virtual std::wstring GetSkinFolder() override;
    virtual std::wstring GetSkinFile() override;
    virtual std::wstring GetWindowClassName() const override;

    /**
    * 收到 WM_CREATE 消息时该函数会被调用,通常做一些控件初始化的操作
    */
    virtual void InitWindow() override;
    /**
    * 收到 WM_CLOSE 消息时该函数会被调用
    */
    virtual LRESULT OnClose(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);

    //其他的功能函数
    void SetBeforeCreate(const AppItem& Item, POINT pt){ item_ = Item; pt_ = pt; }
    void AdjustPos();
    void InstallHook();
    void UnInstallHook();
    static LRESULT CALLBACK LowLevelMouseProc(int nCode, WPARAM wParam, LPARAM lParam);
private:
    AppItem item_;
    ui::Box* origin_owner = nullptr;  //
    POINT pt_;
    static HHOOK mouse_Hook_ ;
    static AppWindow* pThis_;
};

appitem.cpp

#include "stdafx.h"
#include "appitem.h"


AppItemUi* AppItemUi::Create(const AppItem& item)
{
    AppItemUi* uiItem = new AppItemUi;
    uiItem->SetAppdata(item, false);
    ui::GlobalManager::FillBoxWithCache(uiItem, L"movecontrol/app_item.xml");
    return uiItem;
}

void AppItemUi::DoInit()
{
    app_icon_ = static_cast<ui::Control*>(FindSubControl(L"app_icon"));
    if (app_icon_)
    {
        app_icon_->SetBkImage(app_data_._icon);
    }
    app_name_ = static_cast<ui::Label*>(FindSubControl(L"app_name"));
    if (app_name_)
    {
        app_name_->SetText(app_data_._name);
    }

    //绑定事件
     
}

void AppItemUi::SetAppdata(const AppItem& item,bool refresh)
{
    app_data_ = item;
    if (refresh)
    {
        if (app_icon_)
        {
            app_icon_->SetBkImage(app_data_._icon);
        }
        if (app_name_)
        {
            app_name_->SetText(app_data_._name);
        }
    }
}

void AppItemUi::FixPos(int step, int index)
{
    if (index != -1)
    {
        index_ = index;
    }
    index_ += step;

    ui::UiRect marginRect = { (index_ % EACH_LINE)*APP_WIDTH, (index_ / EACH_LINE)*APP_HEIGHT,0,0 };
    
    SetMargin(marginRect);
}



AppWindow::AppWindow()
{

}

AppWindow::~AppWindow()
{

}

std::wstring AppWindow::GetSkinFolder()
{
    return L"movecontrol";
}

std::wstring AppWindow::GetSkinFile()
{
    return L"app_window.xml";
}

std::wstring AppWindow::GetWindowClassName() const
{
    return L"movecontrol";
}

void AppWindow::InitWindow()
{
    ui::VBox* root = static_cast<ui::VBox*>(FindControl(L"root"));
    if (root)
    {
        auto app_item = AppItemUi::Create(item_);
        root->Add(app_item);
    }

    //设置消息钩子,不然无法即时移动
    InstallHook();

    //移动到合适的位置
    AdjustPos();
    
}

LRESULT AppWindow::OnClose(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
    //清理hook
    UnInstallHook();
    pThis_ = nullptr;
    
    return 0;
}

HHOOK AppWindow::mouse_Hook_;

AppWindow* AppWindow::pThis_;

void AppWindow::AdjustPos()
{
    //移动到合适位置,并接管鼠标
    //移植pos的位置,注意去掉阴影
    //
    ui::UiRect rcCorner = GetShadowCorner();
    POINT ptCursor;
    ::GetCursorPos(&ptCursor);
    //左上角的位置
    ptCursor.x -= pt_.x;
    ptCursor.y -= pt_.y;

    ::SetWindowPos(GetHWND(), NULL, ptCursor.x - rcCorner.left, ptCursor.y - rcCorner.top, -1, -1, SWP_NOSIZE | SWP_NOZORDER | SWP_SHOWWINDOW);
}

void AppWindow::InstallHook()
{
    if (mouse_Hook_)   UnInstallHook();
    mouse_Hook_ = SetWindowsHookEx(WH_MOUSE_LL,
        (HOOKPROC)AppWindow::LowLevelMouseProc, GetModuleHandle(NULL), NULL);
}

void AppWindow::UnInstallHook()
{
    if (mouse_Hook_) {
        UnhookWindowsHookEx(mouse_Hook_);
        mouse_Hook_ = NULL;  //set NULL  
    }
}

LRESULT CALLBACK AppWindow::LowLevelMouseProc(int nCode, WPARAM wParam, LPARAM lParam)
{
    if (nCode == HC_ACTION)
    {
        if (wParam == WM_MOUSEMOVE &&::GetKeyState(VK_LBUTTON) < 0)
        {
            MOUSEHOOKSTRUCT *pMouseStruct = (MOUSEHOOKSTRUCT *)lParam;
            if (NULL != pMouseStruct)
            {
                if (pThis_)
                {
                    pThis_->AdjustPos();
                }
            }
        }
        else if (wParam == WM_LBUTTONUP)
        {
            //鼠标弹起,无论什么时候都需要销毁窗口
            if (pThis_)
            {
                //通知主窗口事件
                ::PostMessage(GetParent(pThis_->GetHWND()), WM_LBUTTONUP, 0, 0);
                pThis_->Close();
            }
        }
    }

    return CallNextHookEx(mouse_Hook_, nCode, wParam, lParam);
}

layouts_form.h

#pragma once
#include "AppDb.h"
enum ThreadId
{
    kThreadUI
};
class LayoutsForm : public ui::WindowImplBase
{
public:
    LayoutsForm(const std::wstring& class_name, const std::wstring& theme_directory, const std::wstring& layout_xml);
    ~LayoutsForm();

    /**
     * 一下三个接口是必须要覆写的接口,父类会调用这三个接口来构建窗口
     * GetSkinFolder        接口设置你要绘制的窗口皮肤资源路径
     * GetSkinFile            接口设置你要绘制的窗口的 xml 描述文件
     * GetWindowClassName    接口设置窗口唯一的类名称
     */
    virtual std::wstring GetSkinFolder() override;
    virtual std::wstring GetSkinFile() override;
    virtual std::wstring GetWindowClassName() const override;

    /**
     * 收到 WM_CREATE 消息时该函数会被调用,通常做一些控件初始化的操作
     */
    virtual void InitWindow() override;

    /**
     * 收到 WM_CLOSE 消息时该函数会被调用
     */
    virtual LRESULT OnClose(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);
    /**
    * @brief 接收到鼠标左键弹起消息时被调用
    * @param[in] uMsg 消息内容
    * @param[in] wParam 消息附加参数
    * @param[in] lParam 消息附加参数
    * @param[out] bHandled 返回 true 则继续派发该消息,否则不再派发该消息
    * @return 返回消息处理结果
    */
    virtual LRESULT OnLButtonUp(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);


public:
    static void ShowCustomWindow(const std::wstring& class_name, const std::wstring& theme_directory, const std::wstring& layout_xml);

private:
    //drag-drop相关
    bool OnProcessAppItemDrag(ui::EventArgs* param);
    void DoDrag(ui::Control* pAppItem, POINT pt_offset);
    void DoBeforeDrag();
    void DoDraging(POINT pt_offset);
    bool DoAfterDrag(ui::Box* check);

private:
    std::wstring class_name_;
    std::wstring theme_directory_;
    std::wstring layout_xml_;

    ui::Box* frequent_app_=nullptr;
    ui::Box* my_app_ = nullptr;

    bool is_drag_state_=false;
    POINT old_drag_point_;
    AppItemUi* current_item_ = nullptr;
    
};

layouts_form.cpp

#include "stdafx.h"
#include "layouts_form.h"
using namespace ui;
using namespace std;


LayoutsForm::LayoutsForm(const std::wstring& class_name, const std::wstring& theme_directory, const std::wstring& layout_xml)
    : class_name_(class_name)
    , theme_directory_(theme_directory)
    , layout_xml_(layout_xml)
{
}


LayoutsForm::~LayoutsForm()
{
}

std::wstring LayoutsForm::GetSkinFolder()
{
    return theme_directory_;
}

std::wstring LayoutsForm::GetSkinFile()
{
    return layout_xml_;
}

std::wstring LayoutsForm::GetWindowClassName() const
{
    return class_name_;
}

void LayoutsForm::InitWindow()
{
    //添加应用。应用有可能是服务器下发的,一般本地也有保存的
    //loadFromDb
    //getFromServer---->后台可以先保存到db,再post个消息出来,界面重新从db load。

    //作为demo,先写死
    std::vector<AppItem> applist;
    CAppDb::GetInstance().LoadFromDb(applist);

    frequent_app_ = static_cast<ui::Box*>(FindControl(L"frequent_app"));
    my_app_ = static_cast<ui::Box*>(FindControl(L"my_app"));
    
    for (const auto& item: applist)
    {
        AppItemUi* pAppUi = AppItemUi::Create(item);
        pAppUi->AttachAllEvents(nbase::Bind(&LayoutsForm::OnProcessAppItemDrag, this, std::placeholders::_1));
        if (item._isFrequent)
        {
            pAppUi->FixPos(0, frequent_app_->GetCount());
            frequent_app_->Add(pAppUi);
        }
        else
        {
            pAppUi->FixPos(0, my_app_->GetCount());
            my_app_->Add(pAppUi);
        }
    }
}

LRESULT LayoutsForm::OnClose(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
    PostQuitMessage(0L);
    return __super::OnClose(uMsg, wParam, lParam, bHandled);
}


LRESULT LayoutsForm::OnLButtonUp(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
    if (current_item_ == nullptr)
    {
        return __super::OnLButtonUp(uMsg,wParam,lParam,bHandled);
    }

    Box* pParent = current_item_->GetParent();
    pParent->SetAutoDestroy(true);

    if (!DoAfterDrag(frequent_app_) && !DoAfterDrag(my_app_))
    {
        //回滚
        pParent->AddAt(current_item_, current_item_->getIndex());
        //从index处开始补缺口
        for (int index = current_item_->getIndex()+1; index < pParent->GetCount(); ++index)
        {
            AppItemUi* _pItem = dynamic_cast<AppItemUi*>(pParent->GetItemAt(index));
            if (_pItem)
            {
                _pItem->FixPos(+1);
            }
        }
    }

    //更新App信息到数据库
    CAppDb::GetInstance().SaveToDb(current_item_->getAppData());

    is_drag_state_ = false;
    current_item_ = nullptr;
    SetForegroundWindow(m_hWnd);
    SetActiveWindow(m_hWnd);
    return __super::OnLButtonUp(uMsg, wParam, lParam, bHandled);
}

void LayoutsForm::ShowCustomWindow(const std::wstring& class_name, const std::wstring& theme_directory, const std::wstring& layout_xml)
{
    LayoutsForm* window = new LayoutsForm(class_name, theme_directory, layout_xml);
    window->Create(NULL, class_name.c_str(), WS_OVERLAPPEDWINDOW & ~WS_MAXIMIZEBOX, 0);
    window->CenterWindow();
    window->ShowWindow();
}

//得想办法抓起鼠标弹起的一刻
bool LayoutsForm::OnProcessAppItemDrag(ui::EventArgs* param)
{
    switch (param->Type)
    {
    case kEventMouseMove:
    {
        if (::GetKeyState(VK_LBUTTON) >= 0)
            break;
        if (!is_drag_state_)
        {
            break;
        }
        //检测位移
        LONG cx = abs(param->ptMouse.x - old_drag_point_.x);
        LONG cy = abs(param->ptMouse.y - old_drag_point_.y);
        if (cx < 2 && cy < 2)
        {
            break;
        }
        //在拖拽模式下
        //获取鼠标相对AppItem的位置
        ui::UiRect rect = param->pSender->GetPos(); //左上角有效
        POINT pt = { param->ptMouse.x - rect.left, param->ptMouse.y - rect.top };

        DoDrag(param->pSender, pt);
        is_drag_state_ = false;
    }
        break;
    case kEventMouseButtonDown:
    {
        is_drag_state_ = true;
        old_drag_point_ = param->ptMouse;
    }
        break;
    case kEventMouseButtonUp:
    {
        is_drag_state_ = false;
        //DoDrop

    }
        break;
    }
    return true;
}

void LayoutsForm::DoDrag(ui::Control* pAppItem, POINT pos)
{
    current_item_ = dynamic_cast<AppItemUi*>(pAppItem);
    if (nullptr==current_item_)
    {
        return;
    }
    DoBeforeDrag();
    DoDraging(pos);

}

void LayoutsForm::DoBeforeDrag()
{
    //抠出该项目,后面的项目全部左移
    ASSERT(current_item_);
    if (current_item_)
    {
        Box* pParent = current_item_->GetParent();
        ASSERT(pParent);
        pParent->SetAutoDestroy(false);  //子控件不销毁
        pParent->Remove(current_item_);

        //从index处开始补缺口
        for (int index = current_item_->getIndex(); index < pParent->GetCount(); ++index)
        {
            AppItemUi* _pItem = dynamic_cast<AppItemUi*>(pParent->GetItemAt(index));
            if (_pItem)
            {
                _pItem->FixPos(-1);
            }
        }
    }
}

void LayoutsForm::DoDraging(POINT pos)
{
    //这里注意,如果只是父控件内部移动的话,会简单很多
    //设置下current_item_的setmargin,重新add回去,先保留在父控件的最后一个
    //index_保存之前的位置(防取消),当鼠标弹起时,再设置下合理的值,包括在父控件的位置

    //跨进程移动的话,需要借用drag-drop,也是可以实现的,这里从略

    //本Demo实现的是跨父控件移动(兼容父控件内部移动),并且可以移动出窗口范围,因此创建临时窗口
    //非常遗憾,当临时窗口创建时,临时窗口并没有即时的拖拽感,这里采取Hook方法,在mousemove消息移动。


    //这里创建新窗口 当然得确保不能重复有窗口,这里省略
    AppWindow* pWindow = AppWindow::CreateAppWindow(GetHWND(), pos, current_item_->getAppData());
    ASSERT(pWindow);
}

bool LayoutsForm::DoAfterDrag(ui::Box* check)
{
    //获取鼠标的位置
    POINT pt;
    GetCursorPos(&pt);
    ScreenToClient(m_hWnd, &pt);
    int findIndex = 0;
    UiRect rectBox = check->GetPos();
    if (rectBox.IsPointIn(pt))
    {
        //最好是重合面积更大的,这里根据鼠标位置来了
        for (findIndex = 0; findIndex < check->GetCount(); findIndex++)
        {
            auto control = check->GetItemAt(findIndex);
            UiRect rectCtrl = control->GetPos();
            if (rectCtrl.IsPointIn(pt))
            {
                //插入到该index
                break;
            }
        }
        //合理安排区域
        if (findIndex < check->GetCount())
        {
            current_item_->FixPos(0, findIndex);
            check->AddAt(current_item_, findIndex);
            //从index处开始补缺口
            for (int index = findIndex + 1; index < check->GetCount(); ++index)
            {
                AppItemUi* _pItem = dynamic_cast<AppItemUi*>(check->GetItemAt(index));
                if (_pItem)
                {
                    _pItem->FixPos(+1);
                }
            }
            return true;
        }
        else
        {
            //放到最后面
            current_item_->FixPos(0, findIndex);
            check->Add(current_item_);
            return true;
        }
    }
    else
    {
        return false;
    }
    
}

 

         

posted @ 2019-12-12 14:54  烟波--钓徒  阅读(1331)  评论(0编辑  收藏  举报