C# カーブエディタ作成 進む 戻るの実装

前の記事

bravememo.hatenablog.com

完成図


CurveEditor 進む 戻る機能

外観の作成

この記事と同じやり方でやればできます。

bravememo.hatenablog.com

f:id:Brave345:20200107170113p:plain

外観を作ったらClickイベントを追加させましょう。

ReDo Undoのおおまかなしくみ

これら機能はデザインパターンであるMementoパターンCommandパターンを参考にして制作しました。

techracho.bpsinc.jp

・おおまかなしくみ 戻るの動作は必ずなにか操作をするたびに操作前のデータをスタックに追加し、戻る操作ををしたらスタックからデータをコピーするような感じです。

   /// <summary>
        /// 戻る
        /// </summary>
        public void UnDo()
        {
            //無選択状態に
            m_SelectMode = SelectMode.None;
            CancelMovePoint();
            m_SelectPoint = 0;

            if (m_UnDoStack.Count <= 1)
            {
                //すでに初期状態のグラフデータなら進むスタックに余計なデータを入れさせない
                if (isListMatch(ref m_list, m_UnDoStack.Peek())) return;
                //初期状態のグラフデータ入れる
                m_ReDoStack.Push(new List<BezierPoint>(m_list));
                m_list = new List<BezierPoint>(m_UnDoStack.Peek());//初期状態のグラフ

                return;
            }
            m_ReDoStack.Push(new List<BezierPoint>(m_list));
            m_list = m_UnDoStack.Pop();   //戻る
        }

進むの方は戻るをした際に戻る前のデータをスタックに追加し、進む操作をしたらスタックからデータをコピーします。ほぼ戻ると同じですね。ただ戻ると違い進む前に戻るのスタックに現在のデータを入れる必要があります。でないと一回進むと二度と戻れなくなってしまいます

   /// <summary>
        /// 進む
        /// </summary>
        public void ReDo()
        {
            //一回も戻っていなければ進まない
           if (m_ReDoStack.Count - 1 <= -1) return;
            //無選択状態に
            m_SelectMode = SelectMode.None;
            CancelMovePoint();
            m_SelectPoint = 0;
            m_UnDoStack.Push(new List<BezierPoint>(m_list));//戻るの方にデータを保存
            m_list = m_ReDoStack.Pop();//進む    
        }

・保存データ

保存するデータは 、グラフデータのすべての点情報(List<BezierPoint>)を保存しています。

なぜこれだけにしたかというとこれ以上保存するデータが多いと容量が心配になってくるのとデータの復元作業が複雑になるからです。ようはめんどいから座標データだけにしました。

       /// <summary>
        /// 操作した後のグラフデータをスタックに保存
        /// </summary>
        public void SaveMemento()
        {
            if (m_UnDoStack.Count() != 0)
            {
                //余計なデータを保存していたら削除
                if (isListMatch(ref m_list, m_UnDoStack.Peek()))
                {
                    DeleteMemento();
                    return;
                }
            }
            //データを保存
            m_UnDoStack.Push(new List<BezierPoint>(m_list));
            m_ReDoStack.Clear();
        }

・データを保存するタイミング

これを決めるのが一番大変でした。なぜかというと保存タイミングは基本的にグラフデータに変更があったら保存するのですが、そのタイミングを取得するのが大変でした。 保存タイミングは以下の3つになります。

  1. グラフをクリックしたとき
  2. 点の追加等のボタンが押されたら
  3. ファイルを開くまたは新規作成

クリックしたときの保存が厄介でした。 ダブルクリックで点を追加する機能があるため、ダブルクリック時、同じグラフデータがスタックに貯まってしまう現象がおきました。 なので毎回スタックと現グラフデータを比較して違うときだけ保存させるようにしました。

、がそれではうまくはいきませんでした。 戻るスタックには最新のグラフデータは入ってはいけないため(入ってると2回戻るしないと戻れない)比較してデータが違うときだけ入れるだと最新のグラフデータが戻るスタックに入ってしまい、この実装ではうまくいきませんでした。

そこでデータを保存する際に最新のグラフデータが入ってないかを確認し、入ってたら削除、なければ戻るスタックに入れるようにしました。さらに戻るスタックに入れる際は点を選択しているときだけにすることで(点を選択してないときは必ずグラフデータに変更がかからないため)うまく戻る機能の実現が出来ました。

        /// <summary>
        /// 左マウスクリック後のグラフデータをスタックに保存
        /// </summary>
        public void SaveMemento_Click()
        {
            if (m_UnDoStack.Count() != 0)
            {     //余計なデータを保存していたら削除
                if (isListMatch(ref m_list, m_UnDoStack.Peek())) 
                {
                   DeleteMemento();
                    return;
                }
            }
           //点選択を解除させたときに余計なデータを保存させない
            if (m_SelectMode == SelectMode.None && m_UnDoStack.Count != 0) return;

            m_UnDoStack.Push(new List<BezierPoint>(m_list));
            m_ReDoStack.Clear();
        }

ReDo Undoの実装はめんどい

このカーブエディタで作成で一番めんどかった気がする。 ReDo Undo実装を考えているなら、前もって保存するデータ、保存するタイミングを決めて、設計していくべきだと思いました。

ソースコード

いままで全部ここに書いていて見づらかったので記事の途中にコードを挟んで残りの差分をコードをこちらにのせることにしました。

あと今回のFormはいたるところにセーブ処理を追加して差分だけでも結構長くなってしまうので省略します

       /// <summary>
        /// クリックした瞬間の処理
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void Form1_MouseDown(object sender, MouseEventArgs e)
        {
            switch (e.Button)
            {
                case MouseButtons.Left:
                    m_CurvePointControl.SearchSelectPoint(e);
                    //変更した値を保存
                    m_CurvePointControl.SaveMemento_Click();
                    SaveMousePos(e);
                    break;
                case MouseButtons.Middle:
                    break;
                case MouseButtons.Right:
                    SaveMousePos(e);
                    break;
            }
        }
        /// <summary>
        /// 戻る
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void MenuUnDo_Click(object sender, EventArgs e)
        {
            m_CurvePointControl.UnDo();
            pictureBox1.Refresh();//再描画
        }
        /// <summary>
        /// 進む
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>

        private void MenuReDo_Click(object sender, EventArgs e)
        {
            m_CurvePointControl.ReDo();
            pictureBox1.Refresh();//再描画
        }
  /// <summary>
        /// 余計なデータを保存していたら削除させる
        /// </summary>
        public void DeleteMemento()
        {
            //最初に保存してあるデータは削除させない
            if (m_UnDoStack.Count <= 1) return;        
            m_UnDoStack.Pop();        
        }
        /// <summary>
        ///スタックにたまっているデータを破棄
        /// </summary>
        public void ClearSaveMemento()
        {
            m_UnDoStack.Clear();
            m_ReDoStack.Clear();
        }
        /// <summary>
        /// 今のグラフと前の操作時のグラフが一致しているか
        /// </summary>
        /// <param name="list"></param>
        /// <param name="st"></param>
        /// <returns></returns>
        public bool isListMatch(ref List<BezierPoint> list,  List<BezierPoint> st)
        {
            int listSize = list.Count();
            if (listSize != st.Count()) return false;
            for(int i = 0; i < listSize; i++)
            {
                if (!list[i].Equals(st[i])) return false;
            }
            return true;
        }

終わりに

実は一部戻るに対応してないところがあります。 それはマウスホイールで値を動かしたときです。 マウスホイールが終わったタイミングの取得方法がわからず、現時点では実装が大変そうなので、あとまわしにします。

次かその次くらいで取り合ず最低限やりたかったことは終わらせそう。 だが最後一箇所は苦手な数学を使う所なので結構時間かかりそう。

次の記事 bravememo.hatenablog.com