C#＋Excel：選択したオートシェイプの文字列編集能力を奪い取る

オートシェイプのテキスト絡みで
・Excel2016で、矢印キーでIカーソルが見かけ上動かない問題
・Excel2013で、文字列表示とIカーソル位置が横方向に少しずれる問題
が、くそウザかったので、暫定対策すべくツールをつくってみた。[^1]
（最新版のupdateあてれば治るのかもしれないが・・）

# 画面キャプチャ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/438183/99623bd8-d3b9-f8b2-d7fb-6fb7fb2b14f3.png)

# 使い方

1. Get AutoShapeボタンを押す
2. Excel上で文字列を含むオートシェイプをクリックして選択する
3. Stop/Fixボタンを押す
4. テキストを編集すると、Excel上のオートシェイプのテキストも連動して変化する

## 注意点

- <b>Excelのオートシェイプ上の文字列が、部分ごとに書式が違う場合、書式情報が抹消されます。</b>
- 編集対象のオートシェイプをもつExcelのシート閉じたりオートシェイプを消した状態でテキストを編集すると例外で落ちるはず。
- Excel上でも編集できてしまい、アプリには反映されない。~~（タイトル詐欺）~~

# コンパイル方法

Windows10 Excel2016環境
（Excelがインストールされてれば、環境違っても`C:\Windows\assembly\GAC_MSIL\`以下を`dir /s なんちゃら.dll`で探せば出てくるはず。）

```

csc ^
 /r:C:\Windows\assembly\GAC_MSIL\Microsoft.Office.Interop.Excel\15.0.0.0__71e9bce111e9429c\Microsoft.Office.Interop.Excel.dll ^
 /r:C:\Windows\assembly\GAC_MSIL\office\15.0.0.0__71e9bce111e9429c\Office.dll ^
 %*
```

# C#ソースコード

```csharp

using System;
using System.Drawing;
//using System.Collections.Generic;
//using System.Reflection;
using System.Runtime.CompilerServices; // to use [MethodImpl(MethodImplOptions.NoInlining)]
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using System.Windows.Forms;

using Excel = Microsoft.Office.Interop.Excel;
//using Microsoft.Office.Core;

class ExcelAutoshapeTest : Form
{
    System.Windows.Forms.Timer timer;
    TextBox txtName;
    TextBox txtContent;
    Button btnStartTryGet;
    dynamic _oShape;
    bool _updatedByProgram;

    [MethodImpl(MethodImplOptions.NoInlining)]
    static void DumpTextOfActiveSheet()
    {
        var oExcelApp = (Excel.Application)Marshal.GetActiveObject("Excel.Application");

        var oSheet = (Excel.Worksheet)oExcelApp.ActiveWorkbook.ActiveSheet;

        var oShapes = (Excel.Shapes)oSheet.Shapes;
        foreach ( Excel.Shape oShape in oShapes ) {
            Console.WriteLine(oShape.Type);
            if (oShape.Type == Microsoft.Office.Core.MsoShapeType.msoAutoShape) {
                Console.WriteLine(oShape.AutoShapeType);
                try {
                    dynamic oTextFrame = oShape.TextFrame;
                    Console.WriteLine(oTextFrame.Characters.Text);
                }
                catch(Microsoft.CSharp.RuntimeBinder.RuntimeBinderException) {
                    //Console.WriteLine(e);
                }
                catch(COMException) {
                    //Console.WriteLine(e);
                }
            }
            //oTextFrame.Characters.Text = "bbb";
        }
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    void TryGetActiveAutoShape()
    {
        var oExcelApp = (Excel.Application)Marshal.GetActiveObject("Excel.Application");
        
        dynamic t = oExcelApp.Selection;

        try {
            _updatedByProgram = true;
            _oShape = null;
            txtContent.Text = Regex.Replace(t.Characters.Text, @"\n", "\r\n", RegexOptions.Multiline);
            txtName.Text = t.Name;
            _oShape = t;
        }
        catch(Exception e) {
            if (e is Microsoft.CSharp.RuntimeBinder.RuntimeBinderException ||
                e is COMException ) {  // Note: C#6以降なら when を使ってもう少しきれいに書ける
                
                _oShape = null;
                txtName.Text = "";
                txtContent.Text = "";
                // もみ消す
                return;
            }
            throw e;
        }
        finally {
            _updatedByProgram = false;
        }
    }

    void UpdateExcelAutoShape()
    {
        if ( _oShape != null ) {
            string s = Regex.Replace(txtContent.Text, @"\r\n", "\n", RegexOptions.Multiline);
            _oShape.Characters.Text = s;
        }
    }


    ExcelAutoshapeTest()
    {
        _oShape = null;

        Controls.Add(txtName = new TextBox(){
            Location = new Point(0,0),
            Width = 150,
            ReadOnly = true
        });


        Controls.Add(btnStartTryGet = new Button(){
            Text = "Get AutoShape",
            Location = new Point(180, 0),
            Width = 100
        });
        btnStartTryGet.Click += (s,e)=>{
            if (timer.Enabled) {
                ((Button)s).Text="Get AutoShape";
                txtContent.ReadOnly = false;
                timer.Stop();
            }
            else {
                ((Button)s).Text="Stop/Fix";
                txtContent.ReadOnly = true;
                timer.Start();
            }
        };
        
        
        Controls.Add(txtContent = new TextBox(){
            Location = new Point(0,30),
            ScrollBars = ScrollBars.Both,
            Multiline = true,
            WordWrap = false
        });
        txtContent.TextChanged += (s,e)=>{if(!_updatedByProgram){UpdateExcelAutoShape();}};


        Load      += (s,e)=>{MyResize();};
        Resize    += (s,e)=>{MyResize();};
        ResizeEnd += (s,e)=>{MyResize();};

        timer = new System.Windows.Forms.Timer();
        timer.Interval = 1000;
        timer.Tick += (s,e)=>{
            TryGetActiveAutoShape();
            GC.Collect();
            GC.WaitForPendingFinalizers();
        };
    }

    void MyResize()
    {
        int w = ClientSize.Width;
        int h = ClientSize.Height - txtContent.Top;
        if(w<50){w=50;}
        if(h<50){h=50;}
        txtContent.Size = new Size(w,h);
    }
    
    [STAThread]
    static void Main(string[] args)
    {
        //DumpTextOfActiveSheet();
        Application.Run(new ExcelAutoshapeTest());
    }
}
```


# 参考サイト

- [Visual C# を使用して Office プログラムの実行中のインスタンスを自動化する方法](https://docs.microsoft.com/ja-jp/office/troubleshoot/office-developer/use-visual-c-automate-run-program-instance)
- [C#でエクセルのshapeのテキスト文字を変更したい](https://www.atmarkit.co.jp/bbs/phpBB/viewtopic.php?topic=23270&forum=7&start=8)

[^1]: そもそもExcelは表計算ツールなのでオートシェイプ使う事自体がおかしなことかもしれない