多言語対応(C#、WPF編)

Windows Formsの多言語対応はそういくつも方法がある訳ではありません、以前紹介した方法かちょっとしたバリエーション違い・・程度ではないかと思っています。

一方WPFの場合は表示部(View)をXAMLで構成する都合、そのXAMLのUI要素をどう多言語対応するかにはいくつかの方法が出てきます。私が知り得たところを少しご紹介します。

1.LocBAMLを使う方法

MSDN他、MCTS(70-511)という.NET4.0(WinForms+WPF)の資格試験があったのですが、その試験の自習教材の中でWPFのローカライズ方法としてこのLocBamlを使う方法が紹介されています。この意味でMicrosoftがWPFアプリケーションの多言語対応として推奨(?)想定(?)しているのはこの方法のように感じます。この方法の大まかな手順は、

  1. UIをXAMLで構成する。
  2. MSBuildを使ってローカライズ対象要素にx:UID属性を付与する。
  3. x:UID属性がついた要素をLocBamlツールを使ってcsvとしてエキスポート
  4. csvファイルをローカライズ、ビルドしてサテライトアセンブリを作る

というものです。LocBamlは自分でビルドが必要なツールであることや.NET4.0以降ではそのままでは動作しないとやりにくいところがあるようですが、こちらのサイトでやり方が説明されています。作業が面倒で一部自動化のツール用意が必要など、ローカライズの主流ではないように感じます。

2.Resource.resxを使う方法

VisualStudioのC#プロジェクトに存在するResource.resxを使う方法です。Windows Formsの多言語対応時と文言定義場所位置が同じため、WinFormsアプリとWPFアプリを両方提供する場合には便利そうです。日本のブロガーによく紹介されているようです、この方法の大まかな手順は、

  1. UIをXAMLで構成する
  2. Resource.resxに言語リソースを定義する。
  3. 対応言語分resxファイルの追加、文言定義を繰り返す。
  4. XAMLから文言をStaticResourceで参照する

もともとCodeProjectで紹介された方法のようですが、こちらのサイトでとてもわかりやすく説明されています。

3.リソースディクショナリを使う方法

先に補足ですが、WPFではPenやBrushといったオブジェクトをリソースとして定義できますが、それらをリソースディクショナリという別ファイルにすることが可能です。これは上記2の方法で出てきたResource.resxファイルとは別モノのWPF特有ですのでご注意ください。

リソースディクショナリにはString型、つまり文字列もリソースとして定義可能ですのでこのリソースディクショナリに文言をXAMLで定義しておき、UI要素からリソースとして参照する方法があります。海外のブロガーがよく紹介されているようです、大まかな手順は、

  1. UIをXAMLで構成する
  2. プロジェクトに新しい項目としてリソースディクショナリを追加します。ここにsystem:String型の要素として文字列リソースを定義します。
  3. 対応言語分リソースディクショナリを作成します、このとき同一文言に対しては手順1で割り当てたKeyを合わせておくことが必要です。
  4. 手順1のXAMLのUI要素から必要な文字列をStaticResource or DynamicResourceで参照します。

これはこちらのサイトで紹介されています。2の方法との違いは、resxファイルにリソース定義するかxamlファイルにリソース定義するかの違いです。resxファイルはVisualStudioにエディタがありますが、文言翻訳作業を考えると私はこれがメリットには感じず、XamlのほうがReader/Writerがある分運用しやすく感じます。

ということで3の方法とその運用で使いそうな内容についてちょっとご紹介します。まずUIをXAMLで定義し、翻訳必要な文言を別リソースファイルとして定義するとこのような形になります。

MainWindow.xaml

<Window x:Class="MultiLanguageWpfApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="179" Width="534">
    <Grid>
        <Label Content="{DynamicResource language}" Height="28" HorizontalAlignment="Left" Margin="12,12,0,0" Name="label1" VerticalAlignment="Top" />
        <ComboBox Height="24" HorizontalAlignment="Left" Margin="100,12,0,0" Name="languageComboBox" VerticalAlignment="Top" Width="120" SelectedIndex="0" SelectionChanged="languageComboBoxSelectionChanged">
            <ComboBoxItem>日本語</ComboBoxItem>
            <ComboBoxItem>English</ComboBoxItem>
            <ComboBoxItem>Deutsch</ComboBoxItem>
        </ComboBox>
        <Button Content="{DynamicResource convert}" Height="23" HorizontalAlignment="Left" Margin="396,85,0,0" Name="convertButton" VerticalAlignment="Top" Width="88" Click="convertButton_Click" />
        <Label Content="{DynamicResource input}" Height="28" HorizontalAlignment="Left" Margin="12,66,0,0" Name="label2" VerticalAlignment="Top" />
        <TextBox Height="24" HorizontalAlignment="Left" Margin="100,70,0,0" Name="inputFileTextBox" VerticalAlignment="Top" Width="290" />
        <Label Content="{DynamicResource output}" Height="28" HorizontalAlignment="Left" Margin="12,100,0,0" Name="label3" VerticalAlignment="Top" />
        <TextBox Height="24" HorizontalAlignment="Left" Margin="100,100,0,0" Name="outputFileTextBox" VerticalAlignment="Top" Width="290" />
    </Grid>
</Window>

リソースの実体はそれぞれ以下のような形で定義します。

StringResource.ja-jp.xaml

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:system="clr-namespace:System;assembly=mscorlib">
    <system:String x:Key="language">言語</system:String>
    <system:String x:Key="update">更新</system:String>
    <system:String x:Key="convert">変換</system:String>
    <system:String x:Key="input">入力</system:String>
    <system:String x:Key="output">出力</system:String>
    <system:String x:Key="file">ファイル</system:String>
</ResourceDictionary>

StringResource.en-us.xaml

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:system="clr-namespace:System;assembly=mscorlib">
    <system:String x:Key="language">Language</system:String>
    <system:String x:Key="update">Update</system:String>
    <system:String x:Key="convert">Convert</system:String>
    <system:String x:Key="input">Input</system:String>
    <system:String x:Key="output">Output</system:String>
    <system:String x:Key="file">File</system:String>
</ResourceDictionary>

StringResource.de-DE.xaml

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:system="clr-namespace:System;assembly=mscorlib">
    <system:String x:Key="language">Sprache</system:String>
    <system:String x:Key="update">Aktualisierung</system:String>
    <system:String x:Key="convert">Umwandlung</system:String>
    <system:String x:Key="input">Eingabe</system:String>
    <system:String x:Key="output">Ausgabe</system:String>
    <system:String x:Key="file">Akte</system:String>
</ResourceDictionary>

japan

上のようなUIで、コードビハインドに以下のような簡単な処理を用意しておくと

MainWindow.xaml.cs

namespace MultiLanguageWpfApp
{
    using System;
    using System.Windows;

    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            SetLanguage("ja-jp");
        }
        private void SetLanguage(string cultureCode)
        {
            var dictionary = new ResourceDictionary();
            dictionary.Source = new Uri(@"Resources/StringResource." + cultureCode + @".xaml", UriKind.Relative);
            this.Resources.MergedDictionaries.Add(dictionary);
        }
        private void languageComboBoxSelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e)
        {
            string[] culcureCodes = { "ja-jp", "en-us", "de-DE" };
            SetLanguage(culcureCodes[languageComboBox.SelectedIndex]);
        }
        // xaml-xls変換
        private void convertButton_Click(object sender, RoutedEventArgs e)
        {
            // 後述
        }
    }
}

ドイツ語

コンボボックスアイテム変更で動的な言語切り替えが可能となります。

最後に。私が考える翻訳作業では、ビルド用にはXAMLリソースディクショナリが必要ですが、翻訳業者に提出する資料としたり、文字数制限チェックをしたりと、ベースはExcelファイルの方が都合よさそうです。となるとExcelファイルは読み書きがほしくなるのでNPOIを使って、XAML(リソースディクショナリ)⇔Excel間の変換に以下のようなクラスを使います。

XamlResourceConverter.cs

namespace MultiLanguageWpfApp
{
    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Collections;
    using System.Windows;
    using System.Windows.Markup;
    using NPOI.HSSF.UserModel;
    using NPOI.SS.UserModel;

    public class XamlResourceConverter
    {
        private const int targetColumnIndex = 1;
        private static readonly string sheetName = "sheet1";

        // コンストラクタを外部から隠ぺい
        private XamlResourceConverter() { }

        // ID, 文言のペア
        private Dictionary<string, string> stringTable = new Dictionary<string, string>();

        // Dictionaryから
        public XamlResourceConverter(Dictionary<string, string> table)
        {
            stringTable = table;
        }

        // XAMLファイルから読み込み
        public static XamlResourceConverter FromXamlFile(string xamlResourceFile)
        {
            Dictionary<string, string> table = new Dictionary<string, string>();
            using (FileStream inputStream = new FileStream(xamlResourceFile, FileMode.Open))
            {
                ResourceDictionary xamlTop = XamlReader.Load(inputStream) as ResourceDictionary;
                if (xamlTop == null)
                {
                    throw new InvalidDataException();
                }
                foreach (DictionaryEntry item in xamlTop)
                {
                    table.Add(item.Key as String, item.Value as String);
                }
            }
            return new XamlResourceConverter(table);
        }

        // XLSファイルから読み込み
        public static XamlResourceConverter FromExcelFile(string excelFile)
        {
            Dictionary<string, string> table = new Dictionary<string, string>();
            using (FileStream inputStream = new FileStream(excelFile, FileMode.Open, FileAccess.Read))
            {
                HSSFWorkbook book = new HSSFWorkbook(inputStream);
                ISheet sheet = book.GetSheet(sheetName);
                for (int rowIndex = 0; rowIndex <= sheet.LastRowNum; rowIndex++)
                {
                    IRow row = sheet.GetRow(rowIndex);
                    table.Add(row.GetCell(0).StringCellValue, row.GetCell(1).StringCellValue);
                }
            }
            return new XamlResourceConverter(table);
        }

        // XAMLファイルへの書き込み
        public void SaveToXaml(string xamlResourceFile)
        {
            var resourceDictionary = new ResourceDictionary();
            foreach (var item in stringTable)
            {
                resourceDictionary.Add(item.Key, item.Value);
            }
            var xaml = XamlWriter.Save(resourceDictionary);
            File.WriteAllText(xamlResourceFile, xaml);
        }

        // XLSファイルへの書き込み
        public void SaveToExcel(string excelFile)
        {
            using (FileStream outputStream = new FileStream(excelFile, FileMode.OpenOrCreate))
            {
                // NPOIを使ってExcel出力
                var book = new HSSFWorkbook();
                var sheet = book.CreateSheet(sheetName);
                int rowIndex = 0;
                foreach (KeyValuePair<string,string> item in stringTable)
                {
                    var row = sheet.CreateRow(rowIndex++);
                    var cell1 = row.CreateCell(0, NPOI.SS.UserModel.CellType.String);
                    var cell2 = row.CreateCell(targetColumnIndex, NPOI.SS.UserModel.CellType.String);
                    cell1.SetCellValue(item.Key);
                    cell2.SetCellValue(item.Value);
                }
                book.Write(outputStream);
            }
        }
    }
}

NPOIはxlsxは扱えないみたいで、xls限定になります。他にもWPFの多言語対応化は方法があるようですが、TPOで適宜判断ください。

広告

IEnumerableとIQueryable

ExcelをC#から扱う方法をご紹介しましたが、データ処理をする際にLINQが使えたらと思うのは私だけじゃないはず。以下のテストデータに対してクエリする方法を見ておきます。

 TestData.cs

namespace LinqToEtcetra
{
    using System;

    public class TestData
    {
        public int No { get; set; }
        public string Name { get; set; }
        public DateTime TestDate { get; set; }
        public int Score { get; set; }
    }
}

 

1.Linq To Csv

データ整理にExcelを使う場合、CSVファイルに対してLinqクエリ発行できればと思う時に使えるライブラリがこちら。早速使ってみます。まずNugetで

>PM install-package linqtocsv

としてパッケージをインストールしておきます。テストコードはこちら。

 LinqToCsvTest.cs

namespace LinqToEtcetra
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using LINQtoCSV;

    public class LinqToCsvTest
    {
        private readonly CsvContext csvContext = new CsvContext();
        private readonly CsvFileDescription fileDescription = new CsvFileDescription()
        {
            SeparatorChar = ',',
            FirstLineHasColumnNames = true,
            TextEncoding = Encoding.GetEncoding("Shift-JIS")
        };

        public void WriteFile()
        {
            var testData =  new List<TestData>(){
                new TestData(){No=1, Name="hiro",TestDate=new DateTime(2014,7,14),Score=70},
                new TestData(){No=2, Name="yuki",TestDate=new DateTime(2014,7,14),Score=85},
                new TestData(){No=3, Name="mayu",TestDate=new DateTime(2014,7,14),Score=100},
                new TestData(){No=4, Name="taku",TestDate=new DateTime(2014,7,14),Score=90},
            };
            csvContext.Write<TestData>(testData, "test.csv", fileDescription);
        }

        public void ReadFile()
        {
            var testDataSequence = csvContext.Read<TestData>("test.csv");
            // ご自由にLinqください。
            string maxScorePerson = testDataSequence.OrderByDescending(i => i.Score).First().Name;
            Console.WriteLine("最高得点は" + maxScorePerson + "さんです.");
        }
    }
}

これでもイイのですが、CSVファイルのレイアウトが変わる場合はデータ構造定義しておくことが推奨されているようです。

 TestData.cs(修正)

namespace LinqToEtcetra
{
    using System;
    using LINQtoCSV;

    public class TestData
    {
        [CsvColumn(FieldIndex=1)]
        public int No { get; set; }

        [CsvColumn(FieldIndex = 2)]
        public string Name { get; set; }

        [CsvColumn(FieldIndex = 3, OutputFormat="yyyy/MM/dd hh:mm:ss")]
        public DateTime TestDate { get; set; }

        [CsvColumn(FieldIndex = 4)]
        public int Score { get; set; }
    }
}

書きだされ、読みだされるCSVファイルはこちら。

csv_test

 

2.Linq To Excel

同じデータがExcelファイルとして用意されていたとしましょう。こちらの場合はNugetから

>PM install-package linqtoexcel

としておきます。Linq To Excelの使い方はこちら。

LinqToExcelTest.cs

namespace LinqToEtcetra
{
    using System;
    using System.Linq;
    using LinqToExcel;

    public class LinqToExcelTest
    {
        public void WriteFile()
        {
            throw new NotImplementedException("LinqToExcelではファイル出力はサポートされていません.");
        }

        public void ReadFile()
        {
            ExcelQueryFactory excel = new ExcelQueryFactory("test.xlsx");
            // ご自由にLinqください
            string maxScorePerson = excel.Worksheet("Sheet1").OrderByDescending(i => i["Score"]).First()["Name"];
            Console.WriteLine("最高得点は" + maxScorePerson + "さんです.");
        }
    }
}

想定するExcelファイルは以下です。

test_excel

LinqtoCsvの場合ファイル書き込みがサポートされていましたが、LinqToExcelの場合はサポートされていませんのPIAを使うか、NPOIを使って対応くださいませ。

 

3.IEnumerable と IQueryable

ネット上のいろんな方が記載されているので何も新しくないのですが、IEnumerableとIQueryableの違いはクエリがどこで行われるかです。

IEnumerablevsIQueryable

クエリ結果は同じ。ちなみに先の例で、LinqToCsvはIEnumerableですが、LinqToExcelはIQueryableです。IQueryableのほうが式木作成して外部で適切なフィルタ処理が必要な分、クエリは高速化が見込まれますが作るのが大変です。LinqToCsvとLinqToExcelで差はあるのかは・・・皆様ご自身でご確認ください。

C#からExcelを操作する(VSTO)

Excelレガシーという言葉を以前取り上げましたが、私がそれに苦しめられたのは特定のライブラリへの強い依存のためでした。シリアル通信が目的なだけなのにライブラリを使うためにExcel VBAで構築し、挙句管理されていないコードがあちこちに生まれていました。

じゃあExcelの各セルのデータをシリアル送受信したい場合、どうするか?私ならVSTOを使います。そこで今日は簡単にVSTOをご紹介。今もまだExcel VBAにしがみついている人はそれがベストなのか再度自問して頂くきっかけになればと思います。

VSTOはVisualStudioPro以上(確か。ExpressEditionにはありません)で使える機能です。

vsto_project

【Excelアドイン】 と 【Excelブック】 の2種がありますが、これは適用スコープの違いです。

ExcelアドインはExcel全体に適用されます、Excelシートをpdfとして印刷する機能や、今回紹介するExcel上のデータをシリアル送受信する機能などはアドインとして作るのが望ましいでしょう。対して Excelブックは1ブックにのみ適用されます。従来のExcelVBAはこちらに分類されると考えればイイかと思います。今回はExcelアドインを作成します。

あとちなみにVisualStudioのバージョン次第で対象とできるExcelは変わってきます。私の環境にあるExcel2007(古っ!)に対応しているのはVisualStudio2010だけですので今回はこれでいきます。古いExcelを対象にしたアドインであっても基本的に後方(上位)互換性はあるので、新しいExcelでも使えます。新しいExcel固有の機能だけは新しいExcelを対象にできるVisualStudioでないと操作できないみたいです。

 

1.リボンの構成

新しくプロジェクトを作成してリボンを追加します。WindowsForms同様このリボン上にコントロールを配置すれば良いのですが配置できるものはOfficeリボンコントロールに限定されています。

ribbon_control

serial_ribbon

2.機能の実装

リボンにコントロールを配置したらC#でイベントハンドラを作っていくだけです。

namespace ExcelAddInTest
{
    using System;
    using Microsoft.Office.Tools.Ribbon;
    using System.IO.Ports;
    public partial class TestRibbon
    {
        private void TestRibbon_Load(object sender, RibbonUIEventArgs e)
        {
            // ポート名
            var availablePorts = SerialPort.GetPortNames();
            foreach(var port in availablePorts){
                var item = Globals.Factory.GetRibbonFactory().CreateRibbonDropDownItem();
                item.Label = port;
                portDropDown.Items.Add(item);
            }
            // ボーレート
            var availableBaudRates = new string[]{
                "9600",
                "19200",
                "115200",
            };
            foreach (var baud in availableBaudRates)
            {
                var item = Globals.Factory.GetRibbonFactory().CreateRibbonDropDownItem();
                item.Label = baud;
                baudDropDown.Items.Add(item);
            }
            // パリティ
            var parityOptions = new string[]{
                "Even",
                "Odd",
                "None",
            };
            foreach (var parity in parityOptions)
            {
                var item = Globals.Factory.GetRibbonFactory().CreateRibbonDropDownItem();
                item.Label = parity;
                parityDropDown.Items.Add(item);
            }
        }

        private void startCommButton_Click(object sender, RibbonControlEventArgs e)
        {
            var serialPort = new SerialPort(portDropDown.SelectedItem.Label,
                Int32.Parse(baudDropDown.SelectedItem.Label));
            serialPort.Open();

            // T.B.D. serialPortを使ってRead/Write

            serialPort.Close();
        }
    }
}

簡単ですね。1つ注意点として、上で出てきたRibbonDropDownItemはじめVSTOで利用可能な多くはインターフェースになっています。そのためこれを実装したクラスを作る…とも思いがちですが、そこはファクトリーがありますのでそちらを利用します。

これをビルドすると、

VSTO_SerialComm

アドインとして新しいタブが追加され機能が操作できるようになります。

C#からExcelを操作する(PIAその2)

C#からExcelを操作する方法、前回はWorkSheetの取得までを見てきました。今回はExcel操作の1つ1つについて確認しておきたいと思います。いずれもプロパティの全てを確認することは到底できませんが、関連するクラスを調べていけば所望のプログラムができるかと思います。

ここでは前回のプログラムを引き続き使います。操作FormにWorkSheetオブジェクトは取得できたとして、それらに追加のイベントハンドラを用意する形で個々の操作を見ていきます。見ていく操作は以下になります。

  1. セルにテキストを設定する
  2. セルに数値を設定する
  3. セルに数式を設定する
  4. セルの領域境界に線を引く
  5. チャート(グラフ,表)を挿入する
  6. セルを結合する
  7. セルのフォントを設定する
  8. セルのテキストアライメントを設定する
  9. オートシェイプを挿入する
  10. ハイパーリンクを設定する

 

1.セルにテキストを設定する

セルに文字を設定する方法は前回も出てきましたがこちら。

/// <summary>
/// テキストの設定
/// </summary>
private void setTextButton_Click(object sender, EventArgs e)
{
    xlSheet.Cells[1, 1] = "C#からExcel操作";
    xlSheet.Cells[2, 3] = "GROUP1";
    xlSheet.Cells[2, 4] = "GROUP2";
    xlSheet.Cells[13, 2] = "Sum";
    xlSheet.Cells[15, 2] = "Link";
}

set_text

 

2.セルに数値を設定する

数値を設定する場合もテキストと同じです。

/// <summary>
/// 数値の設定
/// </summary>
private void setNumButton_Click(object sender, EventArgs e)
{
    var random = new Random();
    for (int i = 0; i < 10; i++)
    {
        xlSheet.Cells[i + 3, 2] = i + 1;
        xlSheet.Cells[i + 3, 3] = random.Next(1000);
        xlSheet.Cells[i + 3, 4] = random.Next(1000);
    }
}

set_num

 

3.セルに数式を設定する

数式も同じ。セルの値が変わると自動的に更新されます。

/// <summary>
/// 数式の設定
/// </summary>
private void setFormulaButton_Click(object sender, EventArgs e)
{
    xlSheet.Cells[13, 3] = @"=sum(C3:C12)";
    xlSheet.Cells[13, 4] = @"=sum(D3:D12)";
}

set_formula

 

4.セルの領域境界に線を引く

セルの領域境界に線を引くには範囲を指定してRangeオブジェクトを取得します。

/// <summary>
/// セル境界線の設定
/// </summary>
private void setCellBorderButton_Click(object sender, EventArgs e)
{
    var borderRegion = xlSheet.get_Range("B2", "D13");
    borderRegion.BorderAround();
}

set_range

 

5.チャート(グラフ,表)を挿入する

/// <summary>
/// チャート挿入
/// </summary>
private void insertChartButton_Click(object sender, EventArgs e)
{
    var chart = xlSheet.ChartObjects().Add(250, 20, 300, 250);
    var chartPage = chart.Chart;
    var chartRange = xlSheet.get_Range("C2", "D12");
    chartPage.SetSourceData(chartRange);
    chartPage.ChartType = Excel.XlChartType.xlColumnClustered; 
}

set_chart

 

6.セルを結合する

セルの結合もRangeオブジェクトを取得して行います。

/// <summary>
/// セルのマージ
/// </summary>
private void mergeCellButton_Click(object sender, EventArgs e)
{
    var mergeRange = xlSheet.get_Range("A1", "D1");
    mergeRange.Merge();
}

merge_cell

 

7.セルのフォントを設定する

セルのフォントはセル全体か、セルの一部かで方法が異なります。

/// <summary>
/// フォントの設定
/// </summary>
private void setFontButton_Click(object sender, EventArgs e)
{
    // Cell全体にはFontプロパティ
    xlSheet.Cells[1, 1].Font.Size = 15;
    xlSheet.Cells[1, 1].Font.Bold = true;
    xlSheet.Cells[1, 1].Font.Underline = Excel.XlUnderlineStyle.xlUnderlineStyleDouble;
    xlSheet.Cells[13, 2].Font.OutlineFont = true;

    // Cellの一部にはCharacters
    xlSheet.Cells[1, 1].Characters[1, 2].Font.Color = 255; /* Red */
}

set_font

 

8.セルのテキストアライメントを設定する

/// <summary>
/// アライメントの設定
/// </summary>
private void setAlignmentButton_Click(object sender, EventArgs e)
{
    xlSheet.Cells[1, 1].HorizontalAlignment = Excel.XlHAlign.xlHAlignCenter;
}

set_alignment

 

9.オートシェイプを挿入する

これも前回すこし出てきた内容です。

/// <summary>
/// オートシェイプの挿入
/// </summary>
private void setShapeButton_Click(object sender, EventArgs e)
{
    // 線
    xlSheet.Shapes.AddLine(150, 200, 150, 250);
    // 画像
    xlSheet.Shapes.AddPicture(@"C:\Users\Public\Pictures\Sample Pictures\Lighthouse.jpg",
        Microsoft.Office.Core.MsoTriState.msoTrue,
        Microsoft.Office.Core.MsoTriState.msoTrue,
        100, 250, 100, 100);
}

set_autoshape

 

10.ハイパーリンクを設定する

/// <summary>
/// ハイパーリンクの設定
/// </summary>
private void setHyperLinkButton_Click(object sender, EventArgs e)
{
    xlSheet.Hyperlinks.Add(xlSheet.Cells[15, 2], "https://tocsworld.wordpress.com");
}

set_link

 

ここまで独立して見てきましたが、これらをすべて実行すると以下のようになります。

do_all

今回紹介したのはOfficeのPIAというCOMベースの機能です。Officeには他にもVSTOを使ったものもありますがそちらはまた今度。

C#からExcelを操作する(PIAその1)

個人的な話ですが、ExcelVBAでお粗末なソフトに何度も苦しめられた経験があります。そんな折、Excelレガシーという言葉を知り、激しく共感したところです。Excelを操作するのにVBAだけじゃないんだよ、というメッセージを込めて今日はC#からExcelを操作してみます。

 

1.準備

C#のプロジェクトに参照設定を追加します、COMタブ内 [Microsoft Excel ** Object Library]です。**はバージョンですが、Excel2003ならOffice11、Excel2007ならOffice12、Excel2010ならOffice13を飛ばして14となります。(2003と2007では動作確認しましたが、2010ではできていませんが多分動くはず)

add_re_excel

 

2.起動しているExcelを操作する

まずはExcelが開いており、それに対してデータを書き込む想定です。説明上、1つ1つの操作をわかりやすくする目的でWindowsFormsアプリとしてイベントハンドラに処理を分けて記載してみます。

excel_operate_form

 ExcelOperateForm.cs

namespace ExcelOperation
{
    using System;
    using System.Windows.Forms;
    using System.Runtime.InteropServices;
    using Excel = Microsoft.Office.Interop.Excel;

    public partial class ExcelOperateForm : Form
    {
        Excel._Application xlApp;
        Excel.Workbooks xlBooks;
        Excel.Workbook xlBook;
        Excel.Worksheet xlSheet;

        public ExcelOperateForm()
        {
            InitializeComponent();
        }

        private void getOpeningExcelButton_Click(object sender, EventArgs e)
        {
            // 開いているExcelを取得
            try
            {
                xlApp = (Excel.Application)Marshal.GetActiveObject("Excel.Application");
            }
            catch (COMException ex)
            {
                MessageBox.Show("Excelが起動されていません" + ex.Message);
                return;
            }

            // 開いているExcelのBookを取得
            // <MEMO>
            // Excel操作ソフトを実行中に強制終了などするとCOMオブジェクトの解放モレが発生し、
            // プロセスが残る状態になる。その状態で再度Excel操作をしようとすると、残った
            // プロセスの方を操作対象としてしまう。結果、新しいブックを開いていても以下の操作に
            // 失敗することがある。この場合、タスクマネージャーからEXCEL.EXEプロセスをキルする必要がある。
            xlBooks = xlApp.Workbooks;
            if (xlBooks.Count == 0)
            {
                MessageBox.Show("プロセスが残っていないか確認してください");
                return;
            }
            xlBook = xlBooks.get_Item(1);
            xlSheet = (Excel.Worksheet)xlBook.Sheets.get_Item(1);
        }

        private void setTextButton_Click(object sender, EventArgs e)
        {
            if (xlSheet == null)
            {
                return;
            }
            xlSheet.Cells[1, 1] = "a";
        }

        private void setNumberButton_Click(object sender, EventArgs e)
        {
            if (xlSheet == null)
            {
                return;
            }
            xlSheet.Cells[2, 1] = "10";
        }

        private void setFormulaButton_Click(object sender, EventArgs e)
        {
            if (xlSheet == null)
            {
                return;
            }
            xlSheet.Cells[2, 2] = "=A2*10";
        }

        private void setLineButton_Click(object sender, EventArgs e)
        {
            if (xlSheet == null)
            {
                return;
            }
            xlSheet.Shapes.AddLine(1, 1, 100, 100);
        }
    }
}

注意点としては複数のExcelが起動している場合、それを識別することはできません。プロセス一覧を取得して対応付けて…で可能かもしれませんが茨の道です。そもそもCOMのオートメ-ションなんかにそんな引数もなかったように思うし、そもそもムリかもしれません。

上記コードOffice2003, 2007で動作確認しましたが、一度トラブりました。VisualStudioから実行していて、GetActiveObjectでExcel.Aplicationが取得できないのです。32bit ⇔ 64bitの違いはない筈だし・・・等と考えましたが、答えはStackOverFlow先生にありました。原因はVisualStudioを管理者として実行していたためです。これは気付きづらいですね;

各コマンドを実行したExcelの様子はこちら。

run_excel

 

3.Excelファイルを新規作成して操作する

測定データのロギングなどでは上の方法で良いかとも思いますが、Excelファイルを作成したい場合はちょっとやり方が異なります。こちらも同じ要領でWindowsFormsアプリにしてみます。

excel_create_form

 ExcelCreateForm.cs

namespace ExcelOperation
{
    using System;
    using System.Windows.Forms;
    using System.Runtime.InteropServices;
    using Excel = Microsoft.Office.Interop.Excel;

    public partial class ExcelCreateForm : Form
    {
        Excel._Application xlApp;
        Excel.Workbooks xlBooks;
        Excel.Workbook xlBook;
        Excel.Worksheet xlSheet;

        public ExcelCreateForm()
        {
            InitializeComponent();
        }

        private void createExcelFileButton_Click(object sender, EventArgs e)
        {
            xlApp = new Excel.Application();
            xlBook = xlApp.Workbooks.Add();
            xlSheet = (Excel.Worksheet)xlBook.Sheets.get_Item(1);
        }

        private void setTextButton_Click(object sender, EventArgs e)
        {
            if (xlSheet == null)
            {
                return;
            }
            xlSheet.Cells[1, 1] = "a";
        }

        private void setNumberButton_Click(object sender, EventArgs e)
        {
            if (xlSheet == null)
            {
                return;
            }
            xlSheet.Cells[2, 1] = "10";
        }

        private void setFormulaButton_Click(object sender, EventArgs e)
        {
            if (xlSheet == null)
            {
                return;
            }
            xlSheet.Cells[2, 2] = "=A2*10";
        }

        private void setLineButton_Click(object sender, EventArgs e)
        {
            if (xlSheet == null)
            {
                return;
            }
            xlSheet.Shapes.AddLine(1, 1, 100, 100);
        }

        private void ExcelCreateForm_FormClosing(object sender, FormClosingEventArgs e)
        {
            xlBook.Close();
            xlApp.Quit();
            Marshal.ReleaseComObject(xlApp);
            xlApp = null;
        }
    }
}

これを実行して各ボタンを押下した後Formを閉じようとすると以下のようになります。

save_excel

この辺りはユーザ問い合わせなくファイル保存するとかできますので適宜ケアしてください。ちなみに内部オブジェクトとしてExcelを処理する方法では管理者権限でも問題なく動作します。動作結果は先ほどと同じなので省略。

Excelの操作は他にも色々あるのでExcelVBAを唯一神としないようご注意ください。