【C#】VB.NETのコードを書き直す (2)

Portugal Lisbon Shopping - Free photo on Pixabay - Pixabay C#
Portugal Lisbon Shopping - Free photo on Pixabay - Pixabay

マップエディタの開発後記です。
しばらくはこの関連の記事が続きます。

さて今回も数年前に書いたVB.NETの愚帝(ポンコツ)コードをC#.NETに移植する企画でございますが、今回はマップエディタの要の部分であるマップを作る機能についての寄稿です。

右側にグラフィックチップが並んでいて、選択したものを左側のパネル内に並べていきます。

左側のマップが実際にゲーム内で動く背景になります。
これがマップフィールドです。

これはバイナリデータ*¹なのですが、チップファイルが00~FFまでの16進数が割り当てられていてこの数値を一定のバイト数分敷き詰めたものが上記のマップフィールドとして構成されているわけです。

この話はまだ先に実装予定のバイナリファイル・エクスポート機能(主機能)を実装した時にでもお話しますね(*′ω′*)

*¹ ビューアで見れないファイルです。バイナリファイルだとファイルサイズを抑えられるのと、ファミコンインスパイア作品なので、バイナリデータの羅列が都合がいいのです

マップフィールドの選択制御

今回はこれを実装しました。

元ソースと改修ソース

以下のようになりました。

基本的には一個ずつチップを配置してマップを作るんですが、実際作り始めてみるとそんな繊細な事をやり続けられるわけもなく、
結局のところチップをまとめて配置したり、コピペする事が大半になってきます。

そうなるとやはりまとまった範囲を一括で選択するExcelの範囲選択のような機能が必要です。

実はこの機能、結構実装に手間取ったんですが、なんと当時のVB.NET開発者(私ですけど)はこれを実装していました!

では元のコードです。↓

Partial Class Form1
    ''' <summary>
    ''' Write processing from here When clicking On the created map field.
    ''' <para>PHASE 1. Mousedown in Mapfield.</para>
    ''' </summary>
    ''' <param name="sender"></param>
    ''' <param name="e"></param>
    Private Sub MouseDown_MapTable(sender As Object, e As MouseEventArgs) Handles TableLayoutPanel2.MouseDown
        Dim iMousePoint As Point = System.Windows.Forms.Cursor.Position
        BasePoint = TableLayoutPanel2.PointToClient(iMousePoint)
        Dim temp1 As Integer = Math.Floor(BasePoint.X / 33)
        Dim temp2 As Integer = Math.Floor(BasePoint.Y / 33)
        temp1 = temp1 * 33
        temp2 = temp2 * 33
        BasePoint = New Point(temp1, temp2)
        Select Case e.Button
            Case MouseButtons.Left
                Square1 = 1
            Case MouseButtons.Right
                Square1 = 3
        End Select
    End Sub

    ''' <summary>
    ''' Write processing from here When clicking On the created map field.
    ''' <para>PHASE 2. Move the MouseOver Mapfield.</para>
    ''' </summary>
    ''' <param name="sender"></param>
    ''' <param name="e"></param>
    Private Sub MouseMove_MapTable(sender As Object, e As MouseEventArgs) Handles TableLayoutPanel2.MouseMove
        Dim cOverX, cOverY As Boolean
        Dim rBeginPoint As Point, rEndPoint As Point
        Dim rMousePoint As Point = System.Windows.Forms.Cursor.Position
        Dim graph As Graphics
        rMousePoint = TableLayoutPanel2.PointToClient(rMousePoint)
        'Checking the overfield.
        If rMousePoint.X < 0 Then
            rMousePoint.X = 0 : cOverX = True
        ElseIf rMousePoint.X > 529 Then
            rMousePoint.X = 528 : cOverX = True
        Else
            cOverX = False
        End If
        If rMousePoint.Y < 0 Then
            rMousePoint.Y = 0 : cOverY = True
        ElseIf rMousePoint.Y > 496 Then
            rMousePoint.Y = 495 : cOverY = True
        Else
            cOverY = False
        End If
        If cOverX = False And cOverY = False Then
            PaPen = Pens.Red
            OverField = False
        Else
            PaPen = Pens.DarkGray
            OverField = True
        End If
        If Square1 >= 1 And Square1 <= 4 Then
            graph = TableLayoutPanel2.CreateGraphics()
            graph.DrawRectangle(Pens.DarkGray, ResetRectangle)
            Dim temp1 As Integer, temp2 As Integer
            If rMousePoint.X > BasePoint.X And rMousePoint.Y > BasePoint.Y Then
                rBeginPoint = New Point(BasePoint.X, BasePoint.Y)
                temp1 = Math.Ceiling(rMousePoint.X / 33)
                temp2 = Math.Ceiling(rMousePoint.Y / 33)
                temp1 = temp1 * 33
                temp2 = temp2 * 33
                rEndPoint = New Point(temp1, temp2)
                graph.DrawRectangle(PaPen, rBeginPoint.X, rBeginPoint.Y, rEndPoint.X - rBeginPoint.X, rEndPoint.Y - rBeginPoint.Y)
                ResetRectangle = New Rectangle(rBeginPoint.X, rBeginPoint.Y, rEndPoint.X - rBeginPoint.X, rEndPoint.Y - rBeginPoint.Y)
            ElseIf rMousePoint.X > BasePoint.X And rMousePoint.Y < BasePoint.Y Then
                rBeginPoint = New Point(BasePoint.X, BasePoint.Y + 33)
                temp1 = Math.Ceiling(rMousePoint.X / 33)
                temp2 = Math.Floor(rMousePoint.Y / 33)
                temp1 = temp1 * 33
                temp2 = temp2 * 33
                rEndPoint = New Point(temp1, temp2)
                graph.DrawRectangle(PaPen, rBeginPoint.X, rEndPoint.Y, rEndPoint.X - rBeginPoint.X, rBeginPoint.Y - rEndPoint.Y)
                ResetRectangle = New Rectangle(rBeginPoint.X, rEndPoint.Y, rEndPoint.X - rBeginPoint.X, rBeginPoint.Y - rEndPoint.Y)
            ElseIf rMousePoint.X < BasePoint.X And rMousePoint.Y > BasePoint.Y Then
                rBeginPoint = New Point(BasePoint.X + 33, BasePoint.Y)
                temp1 = Math.Floor(rMousePoint.X / 33)
                temp2 = Math.Ceiling(rMousePoint.Y / 33)
                temp1 = temp1 * 33
                temp2 = temp2 * 33
                rEndPoint = New Point(temp1, temp2)
                graph.DrawRectangle(PaPen, rEndPoint.X, rBeginPoint.Y, rBeginPoint.X - rEndPoint.X, rEndPoint.Y - rBeginPoint.Y)
                ResetRectangle = New Rectangle(rEndPoint.X, rBeginPoint.Y, rBeginPoint.X - rEndPoint.X, rEndPoint.Y - rBeginPoint.Y)
            ElseIf rMousePoint.X < BasePoint.X And rMousePoint.Y < BasePoint.Y Then
                rBeginPoint = New Point(BasePoint.X + 33, BasePoint.Y + 33)
                temp1 = Math.Floor(rMousePoint.X / 33)
                temp2 = Math.Floor(rMousePoint.Y / 33)
                temp1 = temp1 * 33
                temp2 = temp2 * 33
                rEndPoint = New Point(temp1, temp2)
                graph.DrawRectangle(PaPen, rEndPoint.X, rEndPoint.Y, rBeginPoint.X - rEndPoint.X, rBeginPoint.Y - rEndPoint.Y)
                ResetRectangle = New Rectangle(rEndPoint.X, rEndPoint.Y, rBeginPoint.X - rEndPoint.X, rBeginPoint.Y - rEndPoint.Y)
            Else ' MousePoint.X = BasePoint.X Or MousePoint.Y = BasePoint.Y
                ' Processing undefined.
            End If
            If Square1 = 1 OrElse Square1 = 2 Then
                Square1 = 2
            ElseIf Square1 = 3 OrElse Square1 = 4 Then
                Square1 = 4
            End If
        Else ' Square1 = 0
            Square1 = 0
        End If
    End Sub

    ''' <summary>
    ''' Write processing from here When clicking On the created map field.
    ''' <meta>PHASE 3. Determining the selection range and placing the map chip.</meta>
    ''' </summary>
    ''' <param name="sender"></param>
    ''' <param name="e"></param>
    Private Sub MouseUp_MapTable(sender As Object, e As MouseEventArgs) Handles TableLayoutPanel2.MouseUp
        If OverField = False Then
            If Square1 = 2 OrElse Square1 = 4 Then
                Dim temp1 As Integer = ResetRectangle.X \ 33
                Dim temp2 As Integer = ResetRectangle.Y \ 33
                Dim temp3 As Integer = (ResetRectangle.Right - 1) \ 33
                Dim temp4 As Integer = (ResetRectangle.Bottom - 1) \ 33
                Dim pic As PictureBox
                Cursor.Current = Cursors.WaitCursor
                For ii As Integer = temp2 To temp4
                    For jj As Integer = temp1 To temp3
                        pic = TableLayoutPanel2.GetControlFromPosition(jj, ii)
                        If Square1 = 2 Then
                            pic.Tag = PictureBox1.Tag
                            pic.BackgroundImage = PictureBox1.BackgroundImage
                            FAMESharedClasses1.Map_Array(ii + 1, jj + FAMESharedClasses1.CurrentPosition) = pic.Tag
                        Else 'Square1 = 4
                            pic.Tag = -1
                            pic.BackgroundImage = Nothing
                            FAMESharedClasses1.Map_Array(ii + 1, jj + FAMESharedClasses1.CurrentPosition) = pic.Tag
                        End If
                    Next
                Next
                Cursor.Current = Cursors.Default
                Sub_ComponentsReset()
            ElseIf Square1 = 1 OrElse Square1 = 3 Then
                Dim temp1 As Integer = BasePoint.X / 33 : Dim temp2 As Integer = BasePoint.Y / 33
                Dim hand As PictureBox = TableLayoutPanel2.GetControlFromPosition(temp1, temp2)
                Dim iCellPoint As TableLayoutPanelCellPosition = TableLayoutPanel2.GetCellPosition(hand)
                Dim iCellColumn As Integer = iCellPoint.Column : Dim iCellRow As Integer = iCellPoint.Row
                If Square1 = 1 Then
                    hand.Tag = PictureBox1.Tag
                    hand.BackgroundImage = PictureBox1.BackgroundImage
                    FAMESharedClasses1.Map_Array(iCellRow + 1, iCellColumn + FAMESharedClasses1.CurrentPosition) = hand.Tag
                Else 'Square1 = 3
                    hand.Tag = -1
                    hand.BackgroundImage = Nothing
                    FAMESharedClasses1.Map_Array(iCellRow + 1, iCellColumn + FAMESharedClasses1.CurrentPosition) = hand.Tag
                End If
                Sub_ComponentsReset()
            Else 'Square = 0
                'Processing undefined.
            End If
            FAMESharedClasses1.UpdateFlag_Judgement(True)
        Else
            Sub_ComponentsReset()
            OverField = False
        End If
    End Sub

    ''' <summary>
    ''' Deletes an existing rectangular graphic.
    ''' </summary>
    Private Sub Sub_ComponentsReset()
        Dim graph As Graphics
        graph = TableLayoutPanel2.CreateGraphics()
        graph.DrawRectangle(Pens.DarkGray, ResetRectangle)
        Square1 = 0
    End Sub
End Class

これはイベントハンドラです。

MouseDown、MouseMove、MouseUpはエディタのマップフィールドの中にあるTableLayoutPanelコントロールに登録されています。

詳細な説明は省きますが、
MouseDownでは選択したセルの左上の座標点を ‘BasePoint’ メンバへ保持し、
MouseMoveではマウスポインタの位置をセルの右下基準で再計算してRectangle(四角)を描画し、
MouseUpではRectangle領域へ選択中グラフィックチップを配置しているようです。

これはこれで問題なさそうですが、
やはりソースコードのメンテナンス性向上を図っていこうと思います。

C#.NETで書き直しました。↓

/// <summary>
///  Register methods related to the selection range.
/// </summary>
internal partial class MapStructs
{
    private TableLayoutPanel? _mapTable;

    /// <summary>
    ///  The coordinates held to determine the selected area of the map field.
    /// </summary>
    private struct RangeNavigator
    {
        internal Point startCell;
        internal Point endCell;
        internal float dashOffset;
        internal bool isSelecting;
        internal System.Windows.Forms.Timer selectionAnimationTimer;
    }

    /// <summary>
    ///  Constants for invalid values.
    /// </summary>
    private const int VOID = -1;

    /// <summary>
    ///  A shared member set for managing the selected range.
    /// </summary>
    private RangeNavigator _rangeNavigator = new()
    {
        startCell = new(VOID, VOID),
        endCell = new(VOID, VOID),
        dashOffset = 0.0f,
        isSelecting = false,
        selectionAnimationTimer = new()
    };


    /// <summary>
    ///  Unzip the map data file.
    /// </summary>
    /// <param name="path">File path to unzip</param>
    /// <param name="imagelist">A list of extracted graphic images</param>
    /// <param name="objects">A reference to the <see cref="TableLayoutPanel"/> object for adding objects</param>
    /// <returns>True if successful.</returns>
    internal bool Unzip(string path, List<Image> imagelist, ref TableLayoutPanel objects)
    {
        if (null != _binMapFile && _binMapFile.FileOpen(path))
        {
            int row_number = objects.RowCount;
            int col_number = objects.ColumnCount;
            int cellheight = objects.Height / row_number;
            int cellwidth = objects.Width / col_number;
            Size boxsize = new(cellwidth, cellheight);
            int index = 0x00, chipindex = 0x10;

            // An iterative process that sequentially accesses each split panel in a TableLayoutPanel.
            for (int i = 0; i < row_number; i++)
            {
                for (int j = 0; j < col_number; j++)
                {
                    // Place the image container for map data editing in MapFieldTable.
                    byte chipimage = _binMapFile.GetDataByte(chipindex) ?? 0xFF;
                    var chipno = imagelist.Count < chipimage ? imagelist.Count : chipimage;
                    PictureBox picturebox = BinMapFile.CreateTextureBox(index, imagelist[chipno], boxsize);
                    picturebox.MouseDown += MapFieldChip_MouseDown;
                    picturebox.MouseUp += MapFieldChip_MouseUp;
                    picturebox.MouseMove += MapFieldChip_MouseMove;
                    objects.Controls.Add(picturebox, j, i);
                    index++; chipindex++;
                }
            }
            _mapTable = objects;
            SetupMapStructure();
            return true;
        }
        else
        {
            return false;
        }
    }

    /// <summary>
    ///  This is the setup method for the map table object.
    /// </summary>
    private void SetupMapStructure()
    {
        if (null != _mapTable)
        {
            _mapTable.Paint -= MapField_Paint;
            _mapTable.Paint += MapField_Paint;
            _rangeNavigator.selectionAnimationTimer.Interval = 50;      // 50milliseconds.
            _rangeNavigator.selectionAnimationTimer.Tick -= SelectionAnimationTimer_Tick;
            _rangeNavigator.selectionAnimationTimer.Tick += SelectionAnimationTimer_Tick;
        }
    }

    /// <summary>
    ///  Handles the tick event for animating the selection boundary.
    /// </summary>
    /// <param name="sender">The source of the event.</param>
    /// <param name="e">An EventArgs that contains the event data.</param>
    private void SelectionAnimationTimer_Tick(object? sender, EventArgs e)
    {
        _rangeNavigator.dashOffset += 1.0f;
        if (_rangeNavigator.dashOffset > 10.0f) _rangeNavigator.dashOffset = 0;
        _mapTable?.Invalidate(GetSelectionRectangle(_mapTable, _rangeNavigator.startCell, _rangeNavigator.endCell));
    }

    /// <summary>
    ///  This is an indirect event handler that calls the parent control's MapField_MouseDown.
    /// </summary>
    /// <param name="sender"><see cref="PictureBox"/> object</param>
    /// <param name="e">Mouse event args</param>
    private void MapFieldChip_MouseDown(object? sender, MouseEventArgs e)
    {
        if (null != _mapTable)
        {
            Point screenPoint = ((Control)sender!).PointToScreen(e.Location);
            Point tablePoint = _mapTable.PointToClient(screenPoint);
            MapField_MouseDown(sender, new MouseEventArgs(e.Button, e.Clicks, tablePoint.X, tablePoint.Y, e.Delta));
        }
    }

    /// <summary>
    ///  The event when the mouse is pressed down on the MapStruct.
    /// </summary>
    /// <param name="sender">The control of the map field</param>
    /// <param name="e">Mouse event argument</param>
    private void MapField_MouseDown(object? sender, MouseEventArgs e)
    {
        if (_rangeNavigator.selectionAnimationTimer.Enabled)
        {
            ClearSelectionAndRedraw();
            _rangeNavigator.selectionAnimationTimer.Stop();
        }
        Point mousepos = e.Location;
        int rowindex = VOID;
        int colindex = VOID;
        int rowheightsum = 0;
        for (int i = 0; i < _mapTable?.RowCount; i++)
        {
            rowheightsum += (int)_mapTable.RowStyles[i].Height;
            if (mousepos.Y < rowheightsum)
            {
                rowindex = i;
                break;
            }
        }
        int colwidthsum = 0;
        for (int j = 0; j < _mapTable?.ColumnCount; j++)
        {
            colwidthsum += (int)_mapTable.ColumnStyles[j].Width;
            if (mousepos.X < colwidthsum)
            {
                colindex = j;
                break;
            }
        }
        _rangeNavigator.startCell = new Point(colindex, rowindex);
        _rangeNavigator.isSelecting = true;
        _mapTable?.Invalidate(GetSelectionRectangle(_mapTable!, _rangeNavigator.startCell, _rangeNavigator.endCell));
    }

    /// <summary>
    ///  This is an indirect event handler that calls the parent control's MapField_MouseMove.
    /// </summary>
    /// <param name="sender"><see cref="PictureBox"/> object</param>
    /// <param name="e">Mouse event args</param>
    private void MapFieldChip_MouseMove(object? sender, MouseEventArgs e)
    {
        if (null != _mapTable)
        {
            Point screenPoint = ((Control)sender!).PointToScreen(e.Location);
            Point tablePoint = _mapTable.PointToClient(screenPoint);
            MapField_MouseMove(sender, new MouseEventArgs(e.Button, e.Clicks, tablePoint.X, tablePoint.Y, e.Delta));
        }
    }

    /// <summary>
    ///  The event when the mouse is moved over on the MapStruct.
    /// </summary>
    /// <param name="sender">The control of the map field</param>
    /// <param name="e">Mouse event argument</param>
    private void MapField_MouseMove(object? sender, MouseEventArgs e)
    {
        if (_rangeNavigator.isSelecting)
        {
            Point mousepos = e.Location;
            int rowindex = VOID;
            int colindex = VOID;
            int rowheightsum = 0;
            for (int i = 0; i < _mapTable?.RowCount; i++)
            {
                if (mousepos.Y <= rowheightsum + (int)_mapTable.RowStyles[i].Height)
                {
                    rowindex = i;
                    break;
                }
                rowheightsum += (int)_mapTable.RowStyles[i].Height;
            }
            int colwidthsum = 0;
            for (int j = 0; j < _mapTable?.ColumnCount; j++)
            {
                if (mousepos.X <= colwidthsum + (int)_mapTable.ColumnStyles[j].Width)
                {
                    colindex = j;
                    break;
                }
                colwidthsum += (int)_mapTable.ColumnStyles[j].Width;
            }
            _rangeNavigator.endCell = new Point(colindex, rowindex);
            _mapTable?.Invalidate();
            _mapTable?.Update();
        }
    }

    /// <summary>
    ///  Handles the MouseUp event for the MapFieldChip and translates the mouse coordinates to the _mapTable's coordinate system.
    /// </summary>
    /// <param name="sender">The source of the event.</param>
    /// <param name="e">A MouseEventArgs that contains the event data.</param>
    private void MapFieldChip_MouseUp(object? sender, MouseEventArgs e)
    {
        if (null != _mapTable)
        {
            Point screenPoint = ((Control)sender!).PointToScreen(e.Location);
            Point tablePoint = _mapTable.PointToClient(screenPoint);
            MapField_MouseUp(sender, new MouseEventArgs(e.Button, e.Clicks, tablePoint.X, tablePoint.Y, e.Delta));
        }
    }

    /// <summary>
    ///  The event when the mouse is released on the MapStruct.
    /// </summary>
    /// <param name="sender">The control of the map field</param>
    /// <param name="e">Mouse event argument</param>
    private void MapField_MouseUp(object? sender, MouseEventArgs e)
    {
        _rangeNavigator.isSelecting = false;
        _rangeNavigator.selectionAnimationTimer.Start();
    }

    /// <summary>
    ///  The drawing event on the MapStruct.
    /// </summary>
    /// <param name="sender">The control of the map field</param>
    /// <param name="e">Paint event args</param>
    private void MapField_Paint(object? sender, PaintEventArgs e)
    {
        if (_rangeNavigator.startCell.X == VOID || _rangeNavigator.startCell.Y == VOID || _rangeNavigator.endCell.X == VOID || _rangeNavigator.endCell.Y == VOID) return;

        var tables = (TableLayoutPanel)sender!;
        Rectangle area = GetSelectionRectangle(tables, _rangeNavigator.startCell, _rangeNavigator.endCell);
        area = new Rectangle(area.X + 1, area.Y + 1, area.Width - 1, area.Height - 1);
        if (_rangeNavigator.isSelecting)
        {
            using Pen solidPen = new(Color.Blue, 1.0f);
            e.Graphics.DrawRectangle(solidPen, area);
        }
        else
        {
            using Pen dashedPen = new(Color.Blue, 2.0f)
            {
                DashStyle = System.Drawing.Drawing2D.DashStyle.Dash,
                DashOffset = _rangeNavigator.dashOffset
            };
            e.Graphics.DrawRectangle(dashedPen, area);
        }
    }

    /// <summary>
    ///  Returns the rectangle representing the position and size of the specified cell in the given TableLayoutPanel.
    /// </summary>
    /// <param name="panel">The TableLayoutPanel in which the cell is located</param>
    /// <param name="cell">The cell's coordinates (column, row) within the TableLayoutPanel</param>
    /// <returns>A Rectangle representing the position and size of the specified cell. Returns an empty rectangle if the cell's coordinates are invalid.</returns>
    private static Rectangle GetCellRectangle(TableLayoutPanel panel, Point cell)
    {
        if (cell.X == VOID || cell.Y == VOID) return Rectangle.Empty;

        int col = cell.X;
        int row = cell.Y;
        int x = 0;
        int y = 0;
        int[] columnWidths = panel.GetColumnWidths();
        int[] rowHeights = panel.GetRowHeights();
        
        for (int i = 0; i < col; i++)
        {
            x += columnWidths[i];
        }
        for (int j = 0; j < row; j++)
        {
            y += rowHeights[j];
        }
        int width = columnWidths[col];
        int height = rowHeights[row];

        return new Rectangle(x, y, width, height);
    }

    /// <summary>
    ///  Returns the rectangle representing the area between the specified start and end cells in the given TableLayoutPanel.
    /// </summary>
    /// <param name="panel">The TableLayoutPanel in which the cells are located.</param>
    /// <param name="start">The starting cell's coordinates (column, row) within the TableLayoutPanel.</param>
    /// <param name="end">The ending cell's coordinates (column, row) within the TableLayoutPanel.</param>
    /// <returns>A Rectangle representing the combined area between the start and end cells. If the end cell's coordinates are invalid, the start cell's coordinates will be used for both the start and end.</returns>
    private static Rectangle GetSelectionRectangle(TableLayoutPanel panel, Point start, Point end)
    {
        if (end.X == VOID || end.Y == VOID)
            end = start;

        // Get the starting and ending cell rectangles.
        Rectangle startRect = GetCellRectangle(panel, start);
        Rectangle endRect = GetCellRectangle(panel, end);
        // Calculate the four sides of a rectangle and return it.
        int minX = Math.Min(startRect.Left, endRect.Left);
        int minY = Math.Min(startRect.Top, endRect.Top);
        int maxX = Math.Max(startRect.Right, endRect.Right);
        int maxY = Math.Max(startRect.Bottom, endRect.Bottom);
        return new Rectangle(minX, minY, maxX - minX, maxY - minY);
    }

    /// <summary>
    ///  Reset the rectangles cell info.
    /// </summary>
    private void ResetCellPosition()
    {
        _rangeNavigator.startCell = new Point(VOID, VOID);
        _rangeNavigator.endCell = new Point(VOID, VOID);
    }

    /// <summary>
    ///  Clearing and resetting the drawn Rectangle.
    /// </summary>
    internal void ClearSelectionAndRedraw()
    {
        if (null != _mapTable)
        {
            var prevSelectionRect = GetSelectionRectangle(_mapTable, _rangeNavigator.startCell, _rangeNavigator.endCell);
            prevSelectionRect.Inflate(1, 1);
            _mapTable?.Invalidate(prevSelectionRect);
            ResetCellPosition();
        }
    }
}

_mapTable(TableLayoutPanel)の中に追加されているPictureBox(グラフィックチップ)に「MapFieldChip_MouseDown」「MapFieldChip_MouseUp」「MapFieldChip_MouseMove」をそれぞれ割り当ててます。

マジックナンバーを消去

やはり最初に修正したのはマジックナンバーです。
これは書いちゃダメでしょう😅
今日知ってても、明日になったらもう分からない。これ、ネタでもなんでもなくそういうものです。
元のコードは、

BasePoint.X / 33

とにかくこれがいっぱいある。
33ってなんだろう??f(^-^;
他にも ‘Square = 3’ などの謎の数字が…

メソッドの分割(モジュール化)

お次はメソッドの分割です。
とにかくメソッド一本でめっちゃ頑張ってる感が半端ないし、仕様変更を頑なに拒む圧がすごい。。。
まさかソースコード修正なんてないよね?ないんだよね?ね!?とごり押さんばかりに。

private void MapField_Paint(object? sender, PaintEventArgs e)
{
	if (_rangeNavigator.startCell.X == VOID || _rangeNavigator.startCell.Y == VOID || _rangeNavigator.endCell.X == VOID || _rangeNavigator.endCell.Y == VOID) return;

	var tables = (TableLayoutPanel)sender!;
	Rectangle area = GetSelectionRectangle(tables, _rangeNavigator.startCell, _rangeNavigator.endCell);
	area = new Rectangle(area.X + 1, area.Y + 1, area.Width - 1, area.Height - 1);
	if (_rangeNavigator.isSelecting)
	{
		using Pen solidPen = new(Color.Blue, 1.0f);
		e.Graphics.DrawRectangle(solidPen, area);
	}
	else
	{
		using Pen dashedPen = new(Color.Blue, 2.0f)
		{
			DashStyle = System.Drawing.Drawing2D.DashStyle.Dash,
			DashOffset = _rangeNavigator.dashOffset
		};
		e.Graphics.DrawRectangle(dashedPen, area);
	}
}

private static Rectangle GetCellRectangle(TableLayoutPanel panel, Point cell)
{
	if (cell.X == VOID || cell.Y == VOID) return Rectangle.Empty;

	int col = cell.X;
	int row = cell.Y;
	int x = 0;
	int y = 0;
	int[] columnWidths = panel.GetColumnWidths();
	int[] rowHeights = panel.GetRowHeights();
	
	for (int i = 0; i < col; i++)
	{
		x += columnWidths[i];
	}
	for (int j = 0; j < row; j++)
	{
		y += rowHeights[j];
	}
	int width = columnWidths[col];
	int height = rowHeights[row];

	return new Rectangle(x, y, width, height);
}

private static Rectangle GetSelectionRectangle(TableLayoutPanel panel, Point start, Point end)
{
	if (end.X == VOID || end.Y == VOID)
		end = start;

	// Get the starting and ending cell rectangles.
	Rectangle startRect = GetCellRectangle(panel, start);
	Rectangle endRect = GetCellRectangle(panel, end);
	// Calculate the four sides of a rectangle and return it.
	int minX = Math.Min(startRect.Left, endRect.Left);
	int minY = Math.Min(startRect.Top, endRect.Top);
	int maxX = Math.Max(startRect.Right, endRect.Right);
	int maxY = Math.Max(startRect.Bottom, endRect.Bottom);
	return new Rectangle(minX, minY, maxX - minX, maxY - minY);
}

例えば今後、選択範囲を複数個所設定する機能も追加するつもりなのですが、それぞれRectangleを取得できるように ‘GetSelectionRectangle’ メソッドを作っておく事でこれを再利用できるようにしました。

こういうのを構造化プログラミングって言うんですよ( ^ω^)ドヤァ
昔の私は考えが単細胞すぎて笑ってしまいます🤣
動けばそれでいいみたいな。もう自己中も甚だしいですね。

そんなわけでメソッドは分けましょう✨🤗✨

塞がれた穴・・・

VB版エディタを使っている時は自然すぎて気が付かなかったですが、
よく見るとマップフィールドにボーダーラインが入っていました。

どうやらこのボーダーライン上に選択範囲の枠線を描画していたようで、このボーダーラインをなくすと選択範囲は全く映らなくなってしまいました…。

つまり旧エディタは意図的にTableLayoutPanelのボーダーラインを表示しているのです。

今回、私はこのボーダーラインを取り去った上で、選択範囲の枠線描画を行う事にしました。
しかしながらこの思惑が思わぬ問題を引き起こしてしまう事に、私は気が付きませんでした。

なんとTableLayoutPanelにしか選択範囲を引く権利がなく、その上にマップチップというPictureBoxを配置している以上、選択範囲の枠線が最前面に出てこないという問題があるのです。

選択範囲なので、当然ながら複数のPictureBoxを含んだ範囲が選択される想定のはずです。
そうなるとTableLayoutPanel全体の描画制御で行う必要があります。
PictureBoxのPaintイベントでは、単独のPictureBox上でしか制御ができません。それでは複数チップが選択できないのです。

つまり、
選択範囲はTableLayoutPanelのPaintイベントで書く必要があり、
描画位置はチップデータ上に置く必要がある

という解法になるわけです。

ではどうするのでしょうか?

これを実現するために私は小細工をしました。

それはチップデータを半透明にするという荒業です。

/// <summary>
///  Generate a <seealso cref="PictureBox"/> to place the graphics chip.
/// </summary>
/// <param name="address">Hex number to graphic list number</param>
/// <param name="image">Graphic image <seealso cref="Image"/></param>
/// <param name="rectangle">Graphics chip size</param>
/// <returns>Generated <seealso cref="PictureBox"/> object.</returns>
internal static PictureBox CreateTextureBox(int address, Image? image, Size rectangle)
{
    Bitmap bitmap = MakeImageSemiTransparent((Bitmap)image!);
    PictureBox picx = new()
    {
        Name = "Texture_X" + address.ToString(),
        Size = rectangle,
        Margin = new(0),
        Image = bitmap,
    };
    return picx;
}

/// <summary>
///  Creates a semi-transparent version of the provided bitmap image.
/// </summary>
/// <param name="sender">The original bitmap image to make semi-transparent</param>
/// <param name="e">A new bitmap with semi-transparent pixels based on the original image</param>
private static Bitmap MakeImageSemiTransparent(Bitmap original)
{
    Bitmap semiTransparentBitmap = new(original.Width, original.Height);
    for (int i = 0; i < original.Width; i++)
    {
        for (int j = 0; j < original.Height; j++)
        {
            Color originalColor = original.GetPixel(i, j);
            Color semiTransparentColor = Color.FromArgb(128, originalColor.R, originalColor.G, originalColor.B);    // 128 is the alpha value.
            semiTransparentBitmap.SetPixel(i, j, semiTransparentColor);
        }
    }
    return semiTransparentBitmap;
}

‘CreateTextureBox’ メソッドで取得したPictureBoxをTableLayoutPanelの子コントロールとして追加します。(引数の ‘address’ がチップリストの識別番号です)

考えは色々あると思います。

AIチャットで質問すると別の回答が得られましたが、その策は適用できませんでした。
例えば

  • Panelコントロールをチップデータの上に置いて、そこに選択範囲の枠線を描画する
  • TableLayoutPanelのZ-Order(即ち表示順)を調整してチップデータの上に配置する
  • チップデータが描画された後に選択範囲の枠線を描画するように分岐処理をコーディングする

などがありましたが、私のコーディングに問題があったのかもしれません。

今回のエディタでは、選択範囲を指定するモード、マップチップをクリックで配置するモードを切り替えるようなトグルスイッチをフォーム上に置く想定にしていて、選択範囲を指定するモードではチップデータを半透明(カラープロパティにアルファ値を設定)にする結論に至りました。

その他の追加機能

他にもいくつか機能を追加実装しました。

選択範囲をドラッグした後は選択範囲をExcelのようにアニメーションするようにしました。
それは以下のタイマークラスを使用して実現できます。

private void SelectionAnimationTimer_Tick(object? sender, EventArgs e)
{
    _rangeNavigator.dashOffset += 1.0f;
    if (_rangeNavigator.dashOffset > 10.0f) _rangeNavigator.dashOffset = 0;
    _mapTable?.Invalidate(GetSelectionRectangle(_mapTable, _rangeNavigator.startCell, _rangeNavigator.endCell));
}

上記のオフセットは点線の開始ポイントを表すもので、この開始ポイントを
「selectionAnimationTimer.Interval = 50;」
50ミリ秒ごとにずらして再描画する事で枠線がスクロールしているかのように振る舞う仕掛けです。

private void MapFieldChip_MouseDown(object? sender, MouseEventArgs e)
{
    if (null != _mapTable)
    {
        Point screenPoint = ((Control)sender!).PointToScreen(e.Location);
        Point tablePoint = _mapTable.PointToClient(screenPoint);
        MapField_MouseDown(sender, new MouseEventArgs(e.Button, e.Clicks, tablePoint.X, tablePoint.Y, e.Delta));
    }
}

このメソッドはTableLayoutPanel内のセルに追加したコントロール(PictureBox)のイベントハンドラですが、VB.NETとは違いPictureBoxをクリックして選択範囲の位置計算・描画を開始するため、クリックポイントをTableLayoutPanel(ここでは親コントロール)の座標に変換しています。

まだ実装中ですが、選択範囲全てのセルをコピペする機能もアンドゥ/リドゥで時系列移動させる算段を立てています。

やっぱりオブジェクト指向は最適化が必須ですね。

今回の移植は元のVB.NETのコードよりもコード量が増えてしまってますが、これは旧エディタにない機能を追加したのと、メンテナンス性を向上させた代償と言った感じです。

リファクタリングで逆にコードが増えてしまうのはナンセンスだと思われがちですが、実際は保守性を担保するためにファイルが増える事は日常茶飯事です。

会社の面接官だって、決まった事しかできない人間より将来性のある有望な人材を採用するでしょう。
それと同じことです。

とりあえずマップチップを配置できるようにコードを整理したらGitへプッシュして、現在進行中のfeature/mapeditブランチはmasterへマージさせる予定です。

ファイルへの書き出し機能は別ブランチを切って、そちらの工程で作業予定です。

ではまた(^-^)/

コメント

タイトルとURLをコピーしました