• 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;
        }
        
    }

             

  • 相关阅读:
    ER图
    uml图
    第一个迭代任务的制作
    软件测试
    实训记录
    UML系列图——ER图
    UML系列图——用例图
    第一个迭代任务进度
    第一个迭代任务
    需求分析——WBS
  • 原文地址:https://www.cnblogs.com/xuhuajie/p/12029190.html
Copyright © 2020-2023  润新知