マウスの右・左・ホイールで回転、移動、スケーリングする。
#pragma once #ifndef WX_PRECOMP #include <wx/wx.h> #endif #include <wx/dcgraph.h> #include <wx/dcbuffer.h> #include <wx/geometry.h> #include <cmath> #include <wx/gdicmn.h>
//! @brief 画像を回転させる //! @param transform 回転させる画像のアフィン変換行列 //! @param imageCoordPos 回転の中心となる画像座標系の座標 //! @param rad 回転角度(ラジアン) void RotateAroundPoint(wxAffineMatrix2D& transform, const wxPoint2DDouble& imageCoordPos, const double rad); //! @brief 画像をクライアント座標系で移動させる //! @param transform 移動させる画像のアフィン変換行列 //! @param diff 移動量(パネルのクライアント座標系) void TranslateInScreenSpace(wxAffineMatrix2D& transform, const wxPoint& diff); //! @brief クライアント座標を元画像の座標系に変換 //! @param clientPos クライアント座標 //! @param transform 画像のアフィン変換行列 //! @return 元画像の座標系の座標 wxPoint2DDouble ClientToImage(const wxPoint& clientPos, const wxAffineMatrix2D& transform); //! @brief 画像を拡大・縮小する //! @param transform 拡大・縮小する画像のアフィン変換行列 //! @param scalefactor 拡大・縮小率 //! @param clientpos 拡大・縮小の中心座標(クライアント座標系) //! @details clientposに表示されているピクセルが移動しないように拡大・縮小する //! @return なし void Zoom(wxAffineMatrix2D& transform, double scalefactor, const wxPoint clientpos); //! @brief ウィンドウサイズの変化から、拡大・縮小率を計算する //! @param oldSize リサイズ前のウィンドウサイズ //! @param newSize リサイズ後のウィンドウサイズ //! @return 拡大・縮小率 double CalcResizeScaleFactor(const wxSize& oldSize, const wxSize& newSize); //! @brief マウスドラッグによる拡大・縮小率を計算する //! @param oldpos ドラッグ開始時のマウス座標 //! @param newpos ドラッグ中のマウス座標 //! @return 拡大・縮小率 double CalcDragScaleFactor(const wxAffineMatrix2D& transform, const wxPoint& oldpos, const wxPoint& newpos, const int pixelratio); //! @brief ウィンドウサイズが変化したときに、現在画面中央に表示されているピクセルが、リサイズ後も中央に表示されるようにする //! @param oldSize リサイズ前のウィンドウサイズ //! @param newSize リサイズ後のウィンドウサイズ //! @return なし void ResizeKeepCenter(wxAffineMatrix2D& transform, const wxSize& oldSize, const wxSize& newSize); //! @brief 画像をパネルにフィットさせるためのアフィン変換行列を計算 //! @param transform アフィン変換行列 //! @param imageSize 元画像のサイズ //! @param panelSize パネルのサイズ //! @return なし void FitImage(wxAffineMatrix2D& transform, const wxSize& imageSize, const wxSize& panelSize); //! @brief 3点の座標から回転角を計算する //! @param a 1つ目の座標(始点) //! @param b 2つ目の座標(回転中心) //! @param c 3つ目の座標(終点) //! @details cを中心に、aからbへ回転した時の回転角を計算する //! @return 回転角(ラジアン) double CalculateAngle(const wxPoint& a, const wxPoint& b, const wxPoint& c);
//////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////// // マウスによる移動量を角度で計算するクラス class MouseDrag { bool _during; // ドラッグ中かどうか wxPoint _center; // 角度計算の中心座標 wxPoint _from; // 開始地点のマウス座標 wxPoint _to; // 現在のマウス座標 public: MouseDrag(const wxPoint center) : _during(false), _center(center) {} MouseDrag() : _during(false), _center(0, 0) {} //! @brief ドラッグの開始地点 wxPoint GetFrom()const { return _from; } //! @brief ドラッグの現在地点 wxPoint GetTo()const { return _to; } //! @brief ドラッグ開始 void Start(const wxPoint newPos) { _from = newPos; _during = true; } //! @brief ドラッグ終了 void Stop() { _during = false; } //! @brief 現在回転中かどうか bool During() { return _during; } //! @brief マウスの移動量を更新 void Update(const wxPoint& newPos) {_to = newPos;} //! @brief マウスの移動量から角度を計算 double Angle() const{return CalculateAngle(_to, _center, _from);} //! @brief マウスの移動量を取得 wxPoint Difference()const {return _to - _from;} };
class ImagePanel : public wxPanel { enum class DragMode { NONE, MOVE, ROTATE, ZOOM }; wxAffineMatrix2D _transform; //!< アフィン変換行列 wxAffineMatrix2D _transform_back; //!< 移動・回転前のアフィン変換行列のバックアップ wxSize _winsize_back; //!< ウィンドウサイズが変更されるたびに更新されるウィンドウサイズのバックアップ // ウィンドウサイズの変化に関しては、変更開始イベントを拾えないため、変化するたびに前回からの差分に基づいて計算する DragMode _drag_mode; //!< ドラッグモード MouseDrag _mov_dragging; //!< マウスドラッグ間の移動量を計算・管理 std::weak_ptr<wxBitmap> _imageBitmap;//!< 画像データ public: ImagePanel(wxWindow* parent); //! @brief イベントのバインド void EventBind(); private: //! @brief ペイントイベントハンドラ void OnPaint(wxPaintEvent& event); //! @brief イベントハンドラ(移動開始イベントにバインドする用) void OnEventMoveStart(wxMouseEvent& event); //! @brief イベントハンドラ(移動終了イベントにバインドする用) void OnEventMoveStop(wxMouseEvent& event); //! @brief イベントハンドラ(回転開始イベントにバインドする用) void OnEventRotateStart(wxMouseEvent& event); //! @brief イベントハンドラ(回転終了イベントにバインドする用) void OnEventRotateStop(wxMouseEvent& event); //! @brief イベントハンドラ(ドラッグでスケーリング開始イベントにバインドする用) void OnEventScaleStart(wxMouseEvent& event); //! @brief イベントハンドラ(ドラッグでスケーリング終了イベントにバインドする用) void OnEventScaleStop(wxMouseEvent& event); //! @brief マウス移動イベントハンドラ void OnMouseMove(wxMouseEvent& event); //! @brief イベントハンドラ(ホイール回転でスケーリングイベントにバインドする用) void OnEventScaleChange(wxMouseEvent& event); //! @brief ウィンドウサイズ変更イベントハンドラ void OnResize(wxSizeEvent& event); public: //! @brief 表示する画像をセット //! @param bitmap 表示する画像データ //! return なし void SetImage(const std::shared_ptr<wxBitmap>& bitmap); //! @brief 画像が画面に収まるように調節する void FitImageToPanel(); private: //! @brief クライアント領域の中央の座標を取得 //! @return クライアント領域のサイズの1/2 wxPoint GetClientCenter(); //! @brief ドラッグしてズーム処理の開始 //! @param pos ドラッグ開始時のマウス座標 //! @return なし void ZoomStart(const wxPoint& pos); //! @brief ドラッグしてズーム処理の終了 //! @return なし void ZoomStop(); //! @brief ドラッグ中のズーム処理 //! @param pos ドラッグ中のマウス座標 //! @return 画面更新が必要かどうか bool ZoomDuring(const wxPoint& pos); //! @brief ドラッグして移動処理の開始 //! @param pos ドラッグ開始時のマウス座標 //! @return なし void MoveStart(const wxPoint& pos); //! @brief ドラッグして移動処理の終了 //! @return なし void MoveStop(); //! @brief ドラッグして移動中の処理 //! @param pos ドラッグ中のマウス座標 //! @return 画面更新が必要かどうか bool MoveDuring(const wxPoint& pos); //! @brief ドラッグして回転処理の開始 //! @param pos ドラッグ開始時のマウス座標 //! @return なし void RotateStart(const wxPoint& pos); //! @brief ドラッグして回転処理の終了 //! @return なし void RotateStop(); //! @brief ドラッグして回転中の処理 //! @param pos ドラッグ中のマウス座標 //! @return 画面更新が必要かどうか bool RotateDuring(const wxPoint& pos); };
#include "ImagePanel.hpp" ////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////
void RotateAroundPoint(wxAffineMatrix2D& transform, const wxPoint2DDouble& imageCoordPos, const double rad) { // 基準点 (px, py) を原点に移動 transform.Translate(imageCoordPos.m_x, imageCoordPos.m_y); // 回転を適用 (角度はラジアン単位) transform.Rotate(rad); // 元の位置に戻す transform.Translate(-imageCoordPos.m_x, -imageCoordPos.m_y); } void TranslateInScreenSpace(wxAffineMatrix2D& transform, const wxPoint& diff) { // アフィン変換行列の現在の回転・スケーリングを取得 wxMatrix2D mat2D; wxPoint2DDouble translation; transform.Get(&mat2D, &translation); // 回転を取り除くために逆回転行列を作成 double rad = std::atan2(mat2D.m_12, mat2D.m_11); // 現在の回転角を計算(ラジアン) wxAffineMatrix2D inverseRotation; inverseRotation.Rotate(-rad); // スケーリング値を計算 double scaleX = std::sqrt(mat2D.m_11 * mat2D.m_11 + mat2D.m_21 * mat2D.m_21); double scaleY = std::sqrt(mat2D.m_12 * mat2D.m_12 + mat2D.m_22 * mat2D.m_22); // 移動量 (dx, dy) を回転の影響を受けない座標系に変換 wxPoint2DDouble adjustedMove = inverseRotation.TransformPoint(wxPoint2DDouble(diff.x, diff.y)); // 変換行列に移動を適用 transform.Translate(adjustedMove.m_x / scaleX, adjustedMove.m_y / scaleX); } wxPoint2DDouble ClientToImage(const wxPoint& clientPos, const wxAffineMatrix2D& transform) { wxAffineMatrix2D inverse = transform; inverse.Invert(); wxPoint2DDouble imagePos = inverse.TransformPoint(wxPoint2DDouble(clientPos)); return imagePos; } void Zoom(wxAffineMatrix2D& transform, double scalefactor, const wxPoint clientpos) { // クライアント座標を元画像の座標系に変換 wxPoint2DDouble imagePos = ClientToImage(clientpos, transform); // スケーリングの中心をマウス座標に固定 transform.Translate(imagePos.m_x, imagePos.m_y); transform.Scale(scalefactor, scalefactor); transform.Translate(-imagePos.m_x, -imagePos.m_y); } double CalcResizeScaleFactor(const wxSize& oldSize, const wxSize& newSize) { double diffx = newSize.GetWidth() - oldSize.GetWidth(); double diffy = newSize.GetHeight() - oldSize.GetHeight(); double scalex = newSize.GetWidth() / (double)oldSize.GetWidth(); double scaley = newSize.GetHeight() / (double)oldSize.GetHeight(); double scalefactor = 1.0; if ((int)diffy != 0) scalefactor = scaley; return scalefactor; } double CalcDragScaleFactor(const wxAffineMatrix2D& transform, const wxPoint& oldpos, const wxPoint& newpos, const int pixelratio) { // 原点からの距離を計算 double olddist = std::sqrt(oldpos.x * oldpos.x + oldpos.y * oldpos.y); double newdist = std::sqrt(newpos.x * newpos.x + newpos.y * newpos.y); double scalefactor = 1.0; if (olddist < newdist) { // 拡大 scalefactor = 1.0 + (newdist - olddist) / pixelratio; } else { // 縮小 scalefactor = 1.0 - (olddist - newdist) / pixelratio; } return scalefactor; } void ResizeKeepCenter(wxAffineMatrix2D& transform, const wxSize& oldSize, const wxSize& newSize) { wxPoint oldwndCenter = wxPoint(oldSize.GetWidth() / 2, oldSize.GetHeight() / 2); wxPoint newwndCenter = wxPoint(newSize.GetWidth() / 2, newSize.GetHeight() / 2); // リサイズ前の画面中央に表示されていた画像の座標を取得 wxPoint2DDouble oldImageCenter = ClientToImage(oldwndCenter, transform); // 画面サイズの変化に応じた新しい中央座標を取得 wxPoint2DDouble newImageCenter = ClientToImage(newwndCenter, transform); // 変位を計算し、元の画像位置を新しい中央に移動 wxPoint2DDouble shift = oldImageCenter - newImageCenter; transform.Translate(-shift.m_x, -shift.m_y); } void FitImage(wxAffineMatrix2D& transform, const wxSize& imageSize, const wxSize& panelSize) { double scaleX = static_cast<double>(panelSize.GetWidth()) / imageSize.GetWidth(); double scaleY = static_cast<double>(panelSize.GetHeight()) / imageSize.GetHeight(); double scale = std::min(scaleX, scaleY); // 中心を計算 wxPoint offset( (panelSize.GetWidth() - imageSize.GetWidth() * scale) / 2, (panelSize.GetHeight() - imageSize.GetHeight() * scale) / 2); // アフィン変換を設定 transform = wxAffineMatrix2D(); transform.Translate(offset.x, offset.y); transform.Scale(scale, scale); } double CalculateAngle(const wxPoint& a, const wxPoint& b, const wxPoint& c) { wxRealPoint ab(b.x - a.x, b.y - a.y); wxRealPoint cb(b.x - c.x, b.y - c.y); // 内積を計算 double dotProduct = ab.x * cb.x + ab.y * cb.y; // ベクトルの大きさを計算 double magnitudeAB = std::sqrt(ab.x * ab.x + ab.y * ab.y); double magnitudeCB = std::sqrt(cb.x * cb.x + cb.y * cb.y); // コサイン double cosTheta = dotProduct / (magnitudeAB * magnitudeCB); cosTheta = std::min(1.0, std::max(-1.0, cosTheta)); // 角度を計算(ラジアン) double angleRad = std::acos(cosTheta); // 外積を使って角度の符号を決定 double cross = (b.x - a.x) * (c.y - b.y) - (b.y - a.y) * (c.x - b.x); if (cross < 0.0) angleRad = -angleRad; return angleRad;// angleRad * 180.0 / M_PI; }
////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////// ImagePanel::ImagePanel(wxWindow* parent) : wxPanel(parent) { SetBackgroundStyle(wxBG_STYLE_PAINT); SetBackgroundColour(wxColour(0, 0, 0)); _transform = wxAffineMatrix2D(); _drag_mode = DragMode::NONE; EventBind(); } void ImagePanel::OnPaint(wxPaintEvent& event) { wxAutoBufferedPaintDC dc(this); dc.Clear(); std::shared_ptr<wxBitmap> pbitmap = _imageBitmap.lock(); if (!pbitmap) return; wxGraphicsContext* gc = wxGraphicsContext::Create(dc); if (gc) { wxGraphicsMatrix matrix = gc->CreateMatrix(_transform); gc->SetTransform(matrix); gc->DrawBitmap(*pbitmap, 0, 0, pbitmap->GetWidth(), pbitmap->GetHeight()); delete gc; } } void ImagePanel::OnEventMoveStart(wxMouseEvent& event) { MoveStart(event.GetPosition()); CaptureMouse(); } void ImagePanel::OnEventMoveStop(wxMouseEvent& event) { MoveStop(); if (HasCapture()) ReleaseMouse(); } void ImagePanel::OnEventRotateStart(wxMouseEvent& event) { RotateStart(event.GetPosition()); CaptureMouse(); } void ImagePanel::OnEventRotateStop(wxMouseEvent& event) { RotateStop(); if (HasCapture()) ReleaseMouse(); } void ImagePanel::OnEventScaleStart(wxMouseEvent& event) { ZoomStart(event.GetPosition()); CaptureMouse(); } void ImagePanel::OnEventScaleStop(wxMouseEvent& event) { ZoomStop(); if (HasCapture()) ReleaseMouse(); } void ImagePanel::OnMouseMove(wxMouseEvent& event) { if (_mov_dragging.During()) { switch (_drag_mode) { case DragMode::MOVE: if (MoveDuring(event.GetPosition())) { Refresh(); } break; case DragMode::ROTATE: if (RotateDuring(event.GetPosition())) { Refresh(); } break; case DragMode::ZOOM: if (ZoomDuring(event.GetPosition())) { Refresh(); } break; } } } void ImagePanel::OnEventScaleChange(wxMouseEvent& event) { int wheelRotation = event.GetWheelRotation(); // ホイールの回転量によって拡大・縮小率を変更 double scaleFactor = (wheelRotation > 0) ? 1.1 : 0.9; // マウスのクライアント座標を取得 wxPoint clientPos = event.GetPosition(); Zoom(_transform, scaleFactor, clientPos); Refresh(); } void ImagePanel::OnResize(wxSizeEvent& event) { if (GetClientSize().GetWidth() == 0 || GetClientSize().GetHeight() == 0) return; if (_imageBitmap.lock()) { wxSize oldSize = _winsize_back; wxSize newSize = GetClientSize(); ResizeKeepCenter(_transform, oldSize, newSize); double scalefactor = CalcResizeScaleFactor(oldSize, newSize); if ((scalefactor > 0.001) && !isnan(scalefactor) && !isinf(scalefactor)) Zoom(_transform, scalefactor, GetClientCenter()); } // 旧ウィンドウサイズを記録 _winsize_back = GetClientSize(); Refresh(); } ////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////// void ImagePanel::EventBind() { Bind(wxEVT_PAINT, &ImagePanel::OnPaint, this); // 移動に割り当て Bind(wxEVT_LEFT_DOWN, &ImagePanel::OnEventMoveStart, this); Bind(wxEVT_LEFT_UP, &ImagePanel::OnEventMoveStop, this); // 回転に割り当て Bind(wxEVT_RIGHT_DOWN, &ImagePanel::OnEventRotateStart, this); Bind(wxEVT_RIGHT_UP, &ImagePanel::OnEventRotateStop, this); // ホイールドラッグによる拡大・縮小 Bind(wxEVT_MIDDLE_DOWN, &ImagePanel::OnEventScaleStart, this); Bind(wxEVT_MIDDLE_UP, &ImagePanel::OnEventScaleStop, this); /* // 拡大・縮小に割り当て Bind(wxEVT_MOUSEWHEEL, &ImagePanel::OnEventScaleChange, this); */ Bind(wxEVT_MOTION, &ImagePanel::OnMouseMove, this); Bind(wxEVT_SIZE, &ImagePanel::OnResize, this); } void ImagePanel::SetImage(const std::shared_ptr<wxBitmap>& bitmap) { _imageBitmap = bitmap; Refresh(); } void ImagePanel::FitImageToPanel() { std::shared_ptr<wxBitmap> pbitmap = _imageBitmap.lock(); if (!pbitmap) return; // スケール計算 wxSize panelSize = GetClientSize(); wxSize imageSize = pbitmap->GetSize(); FitImage(_transform, imageSize, panelSize); _winsize_back = GetClientSize(); Refresh(); } wxPoint ImagePanel::GetClientCenter() { wxSize panelSize = GetClientSize(); return wxPoint(panelSize.GetWidth() / 2, panelSize.GetHeight() / 2); } void ImagePanel::ZoomStart(const wxPoint& pos) { _drag_mode = DragMode::ZOOM; _transform_back = _transform; _mov_dragging.Start(pos); } void ImagePanel::ZoomStop() { _mov_dragging.Stop(); } bool ImagePanel::ZoomDuring(const wxPoint& pos) { _mov_dragging.Update(pos); wxPoint oldpos = _mov_dragging.GetFrom(); wxPoint newpos = _mov_dragging.GetTo(); wxPoint diff = _mov_dragging.Difference(); if (diff.x == 0 && diff.y == 0) { return false; } wxPoint winoldpos = ClientToScreen(oldpos); wxPoint winnewpos = ClientToScreen(newpos); double scalefactor = CalcDragScaleFactor(_transform, winoldpos, winnewpos, GetClientSize().GetWidth()); if (!isnan(scalefactor) && !isinf(scalefactor)) { _transform = _transform_back; Zoom(_transform, scalefactor, oldpos); } else { return false; } return true; } void ImagePanel::MoveStart(const wxPoint& pos) { _drag_mode = DragMode::MOVE; _transform_back = _transform; _mov_dragging.Start(pos); } void ImagePanel::MoveStop() { _mov_dragging.Stop(); } bool ImagePanel::MoveDuring(const wxPoint& pos) { _mov_dragging.Update(pos); wxPoint diff = _mov_dragging.Difference(); if (diff.x == 0 && diff.y == 0) return false; _transform = _transform_back; // 移動量を画面座標系で適用 TranslateInScreenSpace(_transform, diff); return true; } void ImagePanel::RotateStart(const wxPoint& pos) { _drag_mode = DragMode::ROTATE; _transform_back = _transform; // クライアント領域の中心座標を取得 wxSize panelSize = GetClientSize(); _mov_dragging = MouseDrag(GetClientCenter()); _mov_dragging.Start(pos); } void ImagePanel::RotateStop() { _mov_dragging.Stop(); } bool ImagePanel::RotateDuring(const wxPoint& pos) { _mov_dragging.Update(pos); double rad = _mov_dragging.Angle(); if (isnan(rad) || isinf(rad)) return false; _transform = _transform_back; auto imgpos = ClientToImage(GetClientCenter(), _transform); RotateAroundPoint(_transform, imgpos, rad); return true; }
// 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 <string> #include "ImagePanel.hpp" // ウィンドウ作成 class MyFrame : public wxFrame { ImagePanel* _imagePanel;// 画像表示パネル std::shared_ptr<wxBitmap> _bitmap; public: void PostCreate() { // wxImage::AddHandler(new wxPNGHandler); _bitmap = std::make_shared<wxBitmap>(); wxImage image; if (image.LoadFile(R"(C:\temp\test.png)", wxBITMAP_TYPE_PNG)) {// 画像読み込み *_bitmap = wxBitmap(image); } this->Layout(); // レイアウトの更新 _imagePanel->SetImage(_bitmap);// パネルに画像を指定 _imagePanel->FitImageToPanel(); } MyFrame(const wxString& title, const wxPoint& pos, const wxSize& size) : wxFrame(NULL, wxID_ANY, title, pos, size) { // CallAfter : 現在処理中のイベントが終わったらPostCreateを実行 // コンストラクタはウィンドウ生成イベント扱い CallAfter(&MyFrame::PostCreate); // ImagePanel作成 _imagePanel = new ImagePanel(this); // 画面いっぱいに貼り付ける wxBoxSizer* sizer = new wxBoxSizer(wxHORIZONTAL); sizer->Add(_imagePanel, 1, wxEXPAND); this->SetSizer(sizer); } 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);