スポンサーリンク

wxWidgetsで自作リストボックス作成

何度か作っているが少し真面目に作った。wxSizer等は使っていない。

MyObjectListWidget.hpp

#ifndef WX_PRECOMP
#include <wx/wx.h>
#endif

#include <wx/gdicmn.h> // wxPointに必要
#include <wx/frame.h>  // wxFrameに必要

#include "MyItemPanel.hpp"


//! @brief 自作リストコントロール
class MyObjectListWidget : public wxPanel
{
    wxPanel* _viewArea; //!< リスト表示エリア
    wxScrollBar* _scrollBar; //!< スクロールバー

    int _itemHeight; //!< 一つのアイテムの高さ

    //! @brief 作成したパネルのポインタ一覧
    std::vector< ItemPanel* > _createdPanels;

    //! @brief パネルを生成するオブジェクト
    std::shared_ptr<MyObjectListItemAccessor> _itemaccessor;

    int _activedItemIndex; //!< 現在アクティブなアイテムのインデックス

    //!< パネルを表示
    void SetItemPanels();

    //!< ウィジェット生成直後の動作
    void PostCreate();

public:


    MyObjectListWidget(
        wxWindow* parent,
        std::shared_ptr<MyObjectListItemAccessor> controller,
        int panelheight);

    //! @brief パネル一枚の高さを取得
    int GetPanelHeight()const {
        return _itemHeight;
    }

    //! @brief  panelindex番のパネルが画面から見切れていたら、それが見えるように全体のパネルの配置をずらして調節する
    void FitPosition(int panelindex);

    //! @brief  アクティブなアイテムを指定
    void SetActiveItemIndex(int itemindex) {
        _activedItemIndex = itemindex;
	}

    //! @brief  アクティブなアイテムを取得
    int GetActiveItemIndex()const {
        return _activedItemIndex;
    }

    //! @brief アイテムの総数を取得
    int Count()const;
    
    //! @brief 表示可能なアイテム数を計算
    int GetDisplayableItemCount()const;

    //! @brief パネルの表示状態の更新
    // 使用するパネルのみ表示し、表示するパネルのアイテム番号を指定し、そのパネルの表示内容を更新する
    void UpdateItemPanelList();

    //! @brief サイズ変更イベント
    void OnSize(wxSizeEvent& event);

    //! @brief スクロール
    void OnScroll(wxScrollEvent& event);

};

MyObjectListWidget.cpp

#include "MyObjectListWidget.hpp"




// アイテムの総数を取得
int MyObjectListWidget::Count()const {
    return _itemaccessor->Size();
}
// 表示可能なアイテム数を計算
int MyObjectListWidget::GetDisplayableItemCount()const {
    return _viewArea->GetSize().y / GetPanelHeight() + 1;
}

// パネルを表示
void MyObjectListWidget::SetItemPanels() {
    const int displayable = GetDisplayableItemCount();
    const int created = _createdPanels.size();
    const int panelHeight = GetPanelHeight();
    if (created == displayable) {
        ;
    }
    else if (created < displayable) {
        for (size_t i = 0; i < created; i++) {
            _createdPanels[i]->SetSize(0, i * panelHeight, _viewArea->GetSize().x, panelHeight);
            if (!_createdPanels[i]->IsShown()) {
                _createdPanels[i]->Show();
            }
        }
        // 表示可能なパネル数>定義済みパネル数の場合は
        // 足りていない分のパネルを追加
        for (size_t i = created; i < displayable; i++) {
            
            ItemPanel* itemPanel = _itemaccessor->CreatePanel(_viewArea);
            itemPanel->SetSize(0, i * panelHeight, _viewArea->GetSize().x, panelHeight);
            _createdPanels.push_back(itemPanel);
        }
    }
    else {
        // 表示可能なパネル数<定義済みパネル数の場合は
        // 余ったパネルを非表示
        for (size_t i = displayable; i < created; i++) {
            _createdPanels[i]->Hide();
        }
    }

    // スクロールバーの現在位置
    const int scrollpos = _scrollBar->GetThumbPosition();


    // 表示しているパネル全てについての処理
    // サイズのみ更新
    for (size_t i = 0; i < displayable; i++) {
        _createdPanels[i]->SetSize(0, i * panelHeight, _viewArea->GetSize().x, panelHeight);
        if (!_createdPanels[i]->IsShown()) {
            _createdPanels[i]->Show();
        }
        _createdPanels[i]->SetPanelInfo(this,i, scrollpos + i);
        _createdPanels[i]->UpdatePanel();
    }

}
void MyObjectListWidget::FitPosition(int panelindex) {
    const int panelHeight = GetPanelHeight();


    int fixpixel = 0;

    // アイテムの位置が上方向にはみ出している場合
    if(_createdPanels[panelindex]->GetPosition().y < 0){

        // 修正するピクセル数を計算
        fixpixel = _createdPanels[panelindex]->GetPosition().y;


    }
    // アイテムの位置が下方向にはみ出している場合
    else if (_createdPanels[panelindex]->GetPosition().y + panelHeight > _viewArea->GetSize().y) {

        // 修正するピクセル数を計算
        fixpixel = _createdPanels[panelindex]->GetPosition().y + panelHeight - _viewArea->GetSize().y;
    }

    // 全てのパネルの位置を修正
    for (size_t i = 0; i < _createdPanels.size(); i++) {
        _createdPanels[i]->SetPosition(
            wxPoint(
                _createdPanels[i]->GetPosition().x, 
                _createdPanels[i]->GetPosition().y - fixpixel));
    }
    // 必要な部分だけを再描画
    _viewArea->Refresh();
    _viewArea->Update();
}


void MyObjectListWidget::UpdateItemPanelList() {

    SetItemPanels();

    _viewArea->Layout();
}

void MyObjectListWidget::PostCreate() {
    UpdateItemPanelList();
    _scrollBar->SetScrollbar(
        0, 
        GetDisplayableItemCount(),
        _itemaccessor->Size(),
        5, 
        true);

}


MyObjectListWidget::MyObjectListWidget(
    wxWindow* parent, 
    std::shared_ptr<MyObjectListItemAccessor> controller,
    int panelheight)
    : wxPanel(parent, wxID_ANY), _itemaccessor(controller), _itemHeight(panelheight)
{
    // レイアウトマネージャーを作成
    wxBoxSizer* sizer = new wxBoxSizer(wxHORIZONTAL);

    // ViewAreaパネルを作成して追加
    _viewArea = new wxPanel(this, wxID_ANY);
    sizer->Add(_viewArea, 1, wxEXPAND);

    //_viewAreaの背景を黒
    _viewArea->SetBackgroundColour(wxColour(0, 0, 0));


    // スクロールバーを作成して追加
    _scrollBar = new wxScrollBar(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxSB_VERTICAL);
    sizer->Add(_scrollBar, 0, wxEXPAND);

    // レイアウトを設定
    SetSizer(sizer);

    // サイズ変更時に表示内容の確認と更新
    Bind(wxEVT_SIZE, &MyObjectListWidget::OnSize, this);

    // スクロールイベント
    
    Bind(wxEVT_SCROLL_CHANGED, &MyObjectListWidget::OnScroll, this);
    Bind(wxEVT_SCROLL_LINEUP, &MyObjectListWidget::OnScroll, this);
    Bind(wxEVT_SCROLL_LINEDOWN, &MyObjectListWidget::OnScroll, this);
    Bind(wxEVT_SCROLL_THUMBTRACK, &MyObjectListWidget::OnScroll, this);


    CallAfter(&MyObjectListWidget::PostCreate);

    Layout();

}

// サイズ変更イベント
void MyObjectListWidget::OnSize(wxSizeEvent& event) {

    // ウィンドウの高さが変わった場合、表示可能データ件数が変わり、
    // スクロールバーの最大値が変わるため、再設定
    // このとき、現在の表示位置を変えないように注意する
    _scrollBar->SetScrollbar(
        _scrollBar->GetThumbPosition(),
        GetDisplayableItemCount(),
        _itemaccessor->Size(),
        5,
        true);


    UpdateItemPanelList();
    event.Skip();
}

// スクロール
void MyObjectListWidget::OnScroll(wxScrollEvent& event) {
    UpdateItemPanelList();
    event.Skip();
}

MyItemPanel.hpp

#pragma once

#include <wx/wx.h>

// 自作リストコントロールで一つのアイテムを表示するためのパネル
class ItemPanel :public wxPanel {
private:
    // このパネルが表示上で上から何番目かを表す
    int panelIndex = 0;
    class MyObjectListWidget* m_basepanel;
    size_t _item_index = 0; // このアイテムが指し示すアイテムのインデックス

public:
    ItemPanel(wxWindow* parent);

    //! @brief 自作リストコントロールへのポインタを取得
    class MyObjectListWidget* GetBasePanel() {
        return m_basepanel;
    }

    //! @brief このアイテムのインデックスを取得
    size_t GetItemIndex()const {
        return _item_index;
    }

    //! @brief このパネルが表示上で上から何番目かを取得
    int GetPanelIndex()const {
        return panelIndex;
    }


    //! @brief このアイテムパネルに各種情報をセット
    void SetPanelInfo(class MyObjectListWidget* base, int panelindex, size_t itemindex);

    //! @brief このパネルの表示内容を更新
    void UpdatePanel();


    ///////////////////////////////////////////////////////////////
    ///////////////////////////////////////////////////////////////
    ///////////////////////////////////////////////////////////////
    ///////////////////////////////////////////////////////////////

    virtual size_t Size()const = 0;

    //! @brief itemindex番のアイテムが表示可能か、つまりインデクスの範囲内かをチェック
    //! @param itemindex アイテムのインデックス
    //! @return true: 表示可能 false: 表示不可
    virtual bool IsValidItem(const size_t itemindex) = 0;

    //! @brief パネル一枚の表示を更新
    //! @param panelIndex パネルのインデックス
    //! @param itemindex アイテムのインデックス
    //! @details スクロールが発生するたびにパネル内のウィジェットの表示内容を
    //! itemindex番の内容に更新する
    virtual void UpdatePanelItem(int panelIndex, size_t itemindex) = 0;

    //! @brief パネル一枚の表示を更新(インデクス範囲外の場合)
    //! @param panelIndex パネルのインデックス
    //! @details スクロールが発生して、パネルの表示内容を更新した際に、
    //! itemindex番の内容が存在しない場合に呼び出される
    virtual void UpdatePanelOutOfRange(int panelIndex) = 0;

    //! @brief パネル一枚の再描画の内容を記述する
    //! @param dc 描画コンテキスト
    //! @param panelIndex パネルのインデックス
    //! @param itemindex アイテムのインデックス
    //! @details パネルのOnPaintイベント発生時に呼び出される。
    //! 背景色などを変える場合はここに記載
    virtual void PaintValid(wxDC& dc, int panelIndex, size_t itemindex) = 0;

    //! @brief パネル一枚の再描画の内容を記述する(インデクス範囲外の場合)
    //! @param dc 描画コンテキスト
    //! @param panelIndex パネルのインデックス
    //! @details パネルのOnPaintイベント発生時に呼び出される。
    //! 背景色などを変える場合はここに記載
    //! 特に表示内容がない場合は、背景色を変えるなどして、パネルが無いように見せる
    virtual void PaintOutOfRange(wxDC& dc, int panelIndex) = 0;


    void OnPaint(wxPaintEvent& event);

    //! @brief パネルをクリックしたときの挙動。
    //! 有効なパネル(表示可能なアイテム)をクリックした場合は、OnEventLeftClickを呼び出す)
    //! 無効なパネル(表示不可能なアイテム)をクリックした場合は、OnEventLeftClickOutOfRangeを呼び出す
    virtual void OnLeftClick(wxMouseEvent& event);

    //! @brief 有効なパネルをクリックしたときの挙動
    virtual void OnEventLeftClick(wxMouseEvent& event, int panelIndex, size_t itemindex);

	//! @brief 無効なパネルをクリックしたときの挙動
    virtual void OnEventLeftClickOutOfRange(wxMouseEvent& event, int panelIndex);

};

//! @brief パネルとパネルに表示するデータを関連付けるためのクラス
//! @details パネルの生成時、各パネルはデータへアクセスできなければならない。
//! CreatePanel関数でパネルを作成するときに、データのポインタなどを用いて各パネルがデータへアクセスできるようにする.
class MyObjectListItemAccessor {
public:

    //! @brief パネルを生成する
    virtual ItemPanel* CreatePanel(wxWindow* parent) = 0;
    virtual ~MyObjectListItemAccessor() {};

    //! @brief データの要素数を取得
    virtual size_t Size()const = 0;
};

MyItemPanel.cpp

#include "MyItemPanel.hpp"

#include "MyObjectListWidget.hpp"


ItemPanel::ItemPanel(wxWindow* parent) :
    m_basepanel(nullptr),
    wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL | wxFULL_REPAINT_ON_RESIZE, wxPanelNameStr)
{
    Bind(wxEVT_PAINT, &ItemPanel::OnPaint, this);
    Bind(wxEVT_LEFT_DOWN, &ItemPanel::OnLeftClick, this);

}

void ItemPanel::SetPanelInfo(class MyObjectListWidget* base, int panelindex, size_t itemindex) {
    panelIndex = panelindex;
    _item_index = itemindex;
    m_basepanel = base;
}
void ItemPanel::UpdatePanel() {
    if (IsValidItem(_item_index) == true) {
        UpdatePanelItem(panelIndex, _item_index);
    }
    else {
        UpdatePanelOutOfRange(panelIndex);
    }
}

void ItemPanel::OnPaint(wxPaintEvent& event) {
    ItemPanel* panel = (ItemPanel*)event.GetEventObject();
    MyObjectListWidget* parent = panel->GetBasePanel();

    wxPaintDC dc(panel);
    if (IsValidItem(panel->GetItemIndex()) == true) {
        PaintValid(
            dc,
            panel->GetPanelIndex(),
            panel->GetItemIndex()
        );
    }
    else {
        PaintOutOfRange(
            dc,
            panel->GetPanelIndex()
        );
    }
}

void ItemPanel::OnLeftClick(wxMouseEvent& event) {

    MyObjectListWidget* parent = this->GetBasePanel();
    if (parent) {
        if (IsValidItem(this->GetItemIndex()) == false) {
            OnEventLeftClickOutOfRange(
                event,
                this->GetPanelIndex()
            );
        }
        else {
            OnEventLeftClick(
                event,
                this->GetPanelIndex(),
                this->GetItemIndex()
            );
            parent->FitPosition(this->GetPanelIndex());
        }
    }
}

void ItemPanel::OnEventLeftClick(wxMouseEvent& event, int panelIndex, size_t itemindex) {
    MyObjectListWidget* parent = this->GetBasePanel();
    parent->SetActiveItemIndex(itemindex);

}
void ItemPanel::OnEventLeftClickOutOfRange(wxMouseEvent& event, int panelIndex) {

}

使用例

// https://docs.wxwidgets.org/3.0/overview_helloworld.html

// プリプロセッサに以下二つを追加
// __WXMSW__
// WXUSINGDLL

// サブシステムをWindowsに設定(WinMainで呼び出すので)
// Windows (/SUBSYSTEM:WINDOWS)

#ifndef WX_PRECOMP
#include <wx/wx.h>
#endif

#include <wx/gdicmn.h> // wxPointに必要
#include <wx/frame.h>  // wxFrameに必要


#include"MyObjectListWidget.hpp"

/////////////////////////////////////
/////////////////////////////////////
/////////////////////////////////////

#include <string>



// データ
struct MyData {
    std::string name;
    int index;
};

//! @brief リストの各項目を表示するパネル
class MyItemPanel :public ItemPanel {
    std::weak_ptr<std::vector<MyData>> m_pdata;
    wxStaticText* _label;
public:
    MyItemPanel(wxWindow* parent) : ItemPanel(parent){
        _label = new wxStaticText(this, wxID_ANY, "<unknown>");
        _label->SetPosition(wxPoint(10, 10));

        // ラベルの背景色とテキスト色を設定
        _label->SetBackgroundColour(wxColour(255, 255, 255)); // 白色の背景
        _label->SetForegroundColour(wxColour(0, 0, 0)); // 黒色のテキスト

        _label->Show();
    }
    void setData(std::weak_ptr<std::vector<MyData>> data) {
        m_pdata = data;
    }
    virtual size_t Size()const override { return m_pdata.lock()->size(); };

    virtual bool IsValidItem(const size_t itemindex)override {
        auto datalist = m_pdata.lock();
        if (itemindex < datalist->size())
            return true;
        return false;
    }

    virtual void UpdatePanelItem(int panelIndex, size_t itemindex)override {

        auto data = m_pdata.lock();
        if (_label) {
            _label->SetLabel(
                wxString::Format(
                    "%d Item %s [%d]",
                    panelIndex,
                    (*data)[itemindex].name.c_str(),
                    (*data)[itemindex].index
                )
            );
        }
        // パネルの再描画
        Refresh();
    };

    virtual void UpdatePanelOutOfRange(int panelIndex)override {
        auto data = m_pdata.lock();
        if (_label) {
            _label->SetLabel("out of range");
        }
        // パネルの再描画
        Refresh();

    }
    virtual void PaintValid(wxDC& dc, int panelIndex, size_t itemindex)override {


        dc.SetPen(wxPen(wxColour(0, 0, 0), 1));

        if (itemindex == this->GetBasePanel()->GetActiveItemIndex()) {
            dc.SetBrush(wxBrush(wxColour(255, 0, 0)));
        }
        else {
            dc.SetBrush(wxBrush(wxColour(200, 200, 255)));
        }

        wxSize size = GetSize();
        dc.DrawRectangle(0, 0, size.x, size.y);

    }
    virtual void PaintOutOfRange(wxDC& dc, int panelIndex)override {
        dc.SetPen(wxPen(wxColour(255, 0, 200), 2));
        dc.SetBrush(wxBrush(wxColour(255, 0, 200)));

        wxSize size = GetSize();
        dc.DrawRectangle(0, 0, size.x, size.y);

    }
};

//! @brief リストの各項目を生成するクラス
class MyOLAccessor :public MyObjectListItemAccessor {
    std::weak_ptr<std::vector<MyData>> m_data;
public:
    MyOLAccessor(std::weak_ptr<std::vector<MyData>> data) :
        m_data(data)
    {}

    virtual ItemPanel* CreatePanel(wxWindow* parent)override {
        auto ptr = new MyItemPanel(parent);
        ptr->setData(m_data);
        return ptr;
    }
    virtual size_t Size()const override{
        return m_data.lock()->size();
    }

};


// ウィンドウ作成
class MyFrame : public wxFrame
{
    std::shared_ptr<std::vector<MyData>> m_data;
public:

    void PostCreate() {

        m_data = std::make_shared<std::vector<MyData>>();
        for(size_t i = 0; i < 13; i++) {
            m_data->push_back({ "name " + std::to_string(i), (int)i });
        }

        MyObjectListWidget* objList = new MyObjectListWidget(
            this, 
            std::make_shared<MyOLAccessor>(m_data), // データへアクセス
            80 // アイテムの高さ
        );

        objList->SetSize(0, 0, 200, 200);


        this->Layout(); // レイアウトの更新
    }


    MyFrame(const wxString& title, const wxPoint& pos, const wxSize& size)
        : wxFrame(NULL, wxID_ANY, title, pos, size)
    
    {
        // CallAfter : 現在処理中のイベントが終わったらPostCreateを実行
        // コンストラクタはウィンドウ生成イベント扱い
        CallAfter(&MyFrame::PostCreate);


    }

private:
};

/////////////////////////////////////
/////////////////////////////////////
/////////////////////////////////////

// wxWidgetsのアプリケーション作成
class MyApp : public wxApp {
public:

    virtual bool OnInit() {
        MyFrame* frame = new MyFrame("Hello World", wxPoint(50, 50), wxSize(450, 340));
        frame->Show(true);

        return true;
    }

};

/////////////////////////////////////
/////////////////////////////////////
/////////////////////////////////////

// WinMainをマクロで定義
wxIMPLEMENT_APP(MyApp);

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)


この記事のトラックバックURL: