2016年10月3日月曜日

Office Excel 用アドイン(今日の日付を入力)の作成

Excelで編集しているときに、割とよく入力中のセルに今日の日付を入れたい、と思う時があります。
例えば10月3日なら、10/3 と入力するだけで今日の日付になる、というのは知ってます。
でもね、それをワンタッチで実行したいわけですよ。

VBAとやらでマクロを作ることでもできそうですが、そこは C#使いのあちきのことですから、アドインで実行したい。
ということで、Officeアドインの作り方をまとめてみたいと思います。(前にも書いたような気がするけど気のせいだなw)

まずは Visual Studio (今回は2015)で、新規プロジェクトを作成します。
「新しいプロジェクト」で「Office Addin」-「Excel 2010 VSTO アドイン」を選択します。





アドインプロジェクトができたら、まずリボンを実装してみましょう。
プロジェクトに新しい項目としてリボン(ビジュアルなデザイナー)を追加して、グループボックスとボタンを追加します。

今回、リボン名は TodayRibbon にしました。
ThisAddIn.cs ファイルに次のコードを追加します。
protected override Office.IRibbonExtensibility CreateRibbonExtensibilityObject()
{ 

    return Globals.Factory.GetRibbonFactory().CreateRibbonManager(
        new Microsoft.Office.Tools.Ribbon.IRibbonExtension[] { new TodayRibbon() }

    );
}


ここまでの手順だけでプロジェクトをビルドして実行すると、Excelのリボンに「アドイン」と書かれたタブが追加されます。リボンの追加方法は、あちこちのサイトで説明されていますので他も参考にしてくださいませ。
タブが追加されてボタンが表示されるようになっても、セル上に今日の日付を入力するのに、タブを選択してボタンを押してまたホームタブに戻らなきゃいけない、ってのはとってもめんどくさいですよね?
エクセル標準のリボンタブ「ホーム」にボタンが配置されるようにしたいですよね?
ところが、その方法はググってもあまり説明されていないみたいです。
XAMLを書かなきゃいけないと書いてあるところもありますが、リボンデザイナーで記述することができます。
ここではその辺を詳しく説明したいと思います。
まず必要になるのが、Office 2010 Help Files: Office Fluent User Interface Control Identifiers
です。
こちらからダウンロードできます。
https://www.microsoft.com/en-us/download/details.aspx?id=6627
今回使用するのは、このファイルの中に含まれる、ExcelControls.xlsx ファイルになります。

開いてみましょう。


何やらいろいろ書かれていますが、←部分を見てください。
TabHome           tab
と書かれています。ここから下の部分がExcelを開いたときに表示される、「ホーム」のリボンメニュータブになっています。
次の行には「GroupClipboard」と書かれてますね。エクセルを最初に開くとリボンメニューのもっとも左側に「クリップボード」のグループがあります。
つまり、このリストの順番でコントロールが並べられているわけです。
今回のアドインで作成するボタンは、

このあたりに追加してみたい
ということで、エクセルファイルをスクロールしていくと、
数値タブを示す、[GroupNumber]の group とスタイルタブを示す「GroupStyles」のgroupを見つけることができます。
さて、準備完了です。リボンデザイナーで次のように処理していきます。

1.タブをExcelのホームタブに設定


リボンデザイナの上部をクリックして、プロパティの RibbonType を Microsoft.Excel.Workbookに設定します。
この設定は、リボンがExcelのワークブック用であることを示しています。

2.タブの設定
次にタブメニューを設定します。

リボンデザイナのタブの位置をクリックして、以下のようにプロパティを設定します。
ControlId-ControlIdType をOfficeに設定し、OfficeIdを TabHome に設定します。

TabHome ...どこかで出てきましたね。
そう先ほどの ExcelControls.xlsx ファイルで tab として記載されていた語句です。
この指定で、このタブが、Excelの「ホーム」タブに所属するタブだということが設定されます。

3.グループの設定
最後にグループの設定を行います。もう要領がわかってきたかな?

リボンデザイナでグループボックスをクリックして、プロパティを設定します。
GroupBoxのPositionプロパティのPositionType をBeforeOfficeId として、OfficeIdの部分に GroupStyles を入力します。
これが、ExcelControls.xlsxに記載されていたグループの名前で、その Before、つまり「スタイルタブ」の前に配置する、という意味ですね。
もちろん、AfterOfficeIdを選んで GroupNumberと入力しても同じ位置に表示されます。
(実際には、他のアドインがその場所を横取りする場合があるかもしれません)

ということで、プロジェクトを実行すると、目的の場所に[Today]ボタンが表示されるようになりました。

最後は、[Today]ボタンが押された時の処理を設定します。
リボンデザイナで[Today]ボタンをダブルクリックすると、リボンのコードに次のコードが作成されます。


private void btnToday_Click(object sender, RibbonControlEventArgs e)
{
}


リボンのコードは class TodayRibbon に属していますが、アドインで処理したいエクセルの情報はThisAddIn のインスタンスから参照するほうが楽ですので、次のように ThisAddIn クラスのインスタンスに対して処理を依頼するようにします。

private void btnToday_Click(object sender, RibbonControlEventArgs e)
{
     ThisAddIn logic = ThisAddIn.Instance();
     if(logic != null)
     {
         logic.onButtonToday();
     }
}


ThisAddInのインスタンスを取得できるように、ThisAddIn.cs に次のようなコードを追加します。

public static ThisAddIn instance = null;
public static ThisAddIn Instance()
{
     return instance;
}


instance = this; を ThisAddIn_Startup 内に記述すればOKですね。
※ブレイクポイントを付けてデバッグしてみるとわかるのですが、ThisAddIn_Startup よりも先に CreateRibbonExtensibilityObject が呼び出されてきますので、この位置に instance = this; を付けることもできます。

さて、最後に現在のセル内に今日の日付を設定する処理です。
エクセル用のデータを操作するには、通常、Excel.Workbook オブジェクトを取得し、Excel.WorkSheetオブジェクトを取得し、んでもって...という手順を踏む必要があるのですが、選択中のセルに対する処理では、ThisAddIn.Application.Selection を使用すると現在のセルが取得できました。
ThisAddInには次のコードを記述ました。

internal void onButtonToday()
{
    Excel.Range range = null;
    try
    {
        range = Application.Selection;
        if (range != null)
        {
            range.Value = DateTime.Today;
        }
    }
    finally
    {
        if (range != null)
        {
            Marshal.ReleaseComObject(range);
        }
    }

}


ビルドして実行してみましょう。

うまく動いてくれました。

このアドインのインストーラ欲しい人っているかなー?
求められたら、フリーウェアで公開しますね。

【追記】
上記の実装では、次のような問題があります。
1.IMEがONの場合、全角の日付が入力されるので半角変換しないと日付フォーマットが効かない。
2.アンドゥができない(致命傷w

ということで、これらの問題をいろいろ解決しようと努力しているところで、こんなコマンドを知ることになりました。
[CTRL]+[;](CTRLキーを押しながらセミコロンを押す)

なんと、今日の日付がワンタッチで入力できてしまいました。
ちゃんちゃんw

2016年8月2日火曜日

Visual Studio 2015 C++ で splitpath(関連のライブラリ)が正しく動作しない

いまだに Win32 な開発をするケースがあります。
バージョンアップのついでに、Visual Studio 2008の環境で構築されたプロジェクトを Visual Studio 2015 に移行してビルドしてみました。
とりあえず、動くようになるまでの手続きはさほどややこしくなかったんですけどね。

ファイル名の処理で、「管理表.xls」とか「申請書.doc」なんかでファイル名が正しく処理されなくなる!

なんとも懐かしい、Shift-JIS の \x5c 問題です。

いや、デグレードは困りますよ、マイクロソフトさん!

Visual Studio 2015 Update2 なんですが、ふと facebookを見ると、Update3 のニュースが!
で、MSDN見に行ってみると、Update3が出てるんですね。
んでも、サブスクリプションが期限切れでダウンロードできませんでした。 orz

かくなる上は、ということで、問題となっていた splitpath の部分だけ自前で書くことにしましたよ。
時間かかったけど、リリースできたからいいや。

ということで、ソース書いておきますね。(使ってないモジュールも入ってるけど)

#define IS_MBS_LEAD(c) (((0x81 <= ((unsigned char)(c))) && (((unsigned char)(c)) <= 0x9F)) || ((0xE0 <= ((unsigned char)(c))) && (((unsigned char)(c)) <= 0xFC) ))
#define IS_MBS_TRAIL(c) ((0x7F != (unsigned char)(c)) && (0x40 <= ((unsigned char)(c))) && (((unsigned char)(c)) <= 0xFC))
static const char *previousChar(const char *top, const char *pos)
{
 pos--;
 if (IS_MBS_TRAIL(*pos))
 {
  if (top < pos)
  {
   if (IS_MBS_LEAD(*(pos - 1)))
   {
    return pos - 1;
   }
  }
 }
 return pos;
}

static const char *nextChar(const char *pos, const char *tail)
{
 pos++;
 if (IS_MBS_LEAD(*pos))
 {
  if (pos < tail)
  {
   if (IS_MBS_TRAIL(*(pos + 1)))
   {
    return pos + 1;
   }
  }
 }
 return pos;
}

void SplitPath(const char *path, char *drive, size_t drive_length, char *dir, size_t dir_length, char *fname, size_t fname_length, char *ext, size_t ext_length)
{
 const char *tail = &path[strlen(path)];
 const char *prev = tail;
 size_t length = 0;
 bool fext = false;
 bool ffname = false;
 // EXT
 const char *ext_tail = tail;
 const char *ext_top = tail;
 const char *fname_tail = tail;
 const char *fname_top = tail;
 while ((prev = previousChar(path, prev)) != path)
 {
  if (*prev == '.') 
  {
   ext_top = prev;
   break;
  }
  // 拡張子のないファイルだった場合
  if ((*prev == '\\') || (*prev == '/') || (*prev == ':'))
  {
   prev++;
   break;
  }
 }
 // 拡張子が決定しているか
 const char *src;
 if (ext_top < ext_tail) 
 {
  if (ext != NULL) 
  {
   length = 0;
   src = ext_top;
   while ((src < ext_tail) && (length < fname_length))
   {
    *ext++ = *src++;
    length++;
   }
   *ext = '\0';
  }
  fname_tail = ext_top;
 }
 // ファイル名を取得
 if ((prev != path) && (*prev != '\\') && (*prev != '/') && (*prev != ':'))
 {
  do{
   prev = previousChar(path, prev);
   // 拡張子のないファイルだった場合
   if ((*prev == '\\') || (*prev == '/') || (*prev == ':'))
   {
    prev++;
    break;
   }
  } while (prev != path);
 }
 fname_top = prev;
 if(fname_top < fname_tail)
 {
  if (fname != NULL)
  {
   length = 0;
   src = fname_top;
   while ((src < fname_tail) && (length < fname_length))
   {
    *fname++ = *src++;
    length++;
   }
   *fname = '\0';
  }
 }

 const char *dir_tail = fname_top;
 const char *dir_top = path;
 const char *drive_tail = strchr(path, ':');
 // drive
 if (drive_tail != NULL)
 {
  if (drive != NULL) 
  {
   length = 0;
   src = path;
   while ((src <= drive_tail) && (length < drive_length))
   {
    *drive++ = *src++;
    length++;
   }
   *drive = '\0';
  }
  dir_top = drive_tail + 1;
 }
 if(dir_top < dir_tail)
 {
  if (dir != NULL) 
  {
   length = 0;
   src = dir_top;
   while ((src < dir_tail) && (length < dir_length))
   {
    *dir++ = *src++;
    length++;
   }
   *dir = '\0';
  }
 }
 return;
}

2016年6月13日月曜日

MailItem.Copy() でコピーされたメールを MailItem.Delete()で削除するとオリジナルが消えてしまう?

Outlook のアドイン開発では、メールアイテム(Outlook.MailItem)のインターフェースを多用します。

メールのサイズを取得するには、MailItem.Size オブジェクトを参照すればいいのだけど、新規に作成され編集中のメールの場合、保存処理がされていないため、MailItem.Size には 0 が設定されてしまっています。
この値を取得するため、コピーメールを保存して値を取得後、削除してしまえばいいじゃん。
ということで、次のようなコードを書いたのでした。

        public static ulong GetMailSize(Outlook.MailItem item)
        {
            ulong size = 0;
            //
            //削除済みアイテム格納フォルダを取得する
            Outlook.MAPIFolder dustboxFolder = getDustboxFolder(item.Application, item.SendUsingAccount);
            try
            {
                // Copy+Save+Deleteによるメールサイズの取得
                if (dustboxFolder != null)
                {
                    //メールのコピーを作成する。
                    Outlook.MailItem dummy = item.Copy() as Outlook.MailItem;
                    //メールを保存する
                    dummy.Save();             
                    //メールのサイズを取得。
                    size = (uint)dummy.Size;
                    //削除済みメールかどうかを検査
                    Outlook.MAPIFolder parentFolder = dummy.Parent as Outlook.MAPIFolder;
                    if (dustboxFolder.EntryID != parentFolder.EntryID)
                    {
                        //コピーしたメールを「削除済みアイテム」に移動させる
                        dummy.Move(dustboxFolder);
                    }
                    //削除済みアイテムのコピーしたメールの要素番号を取得
                    int index = getFolderIndex(dustboxFolder, dummy);
                    //コピーしたメールを削除する(※ここ!)
                    dummy.Delete();
                    if (0 < index)
                    {
                        //削除済みアイテムの保存メールを削除する
                        dustboxFolder.Items.Remove(index);
                    }
                    return size;
                }
            }
            catch (Exception e)
            {
                throw e;
            }
     return 0;
    }

なんとまぁめんどくさい処理なんでしょ。
で、こいつを実行するとですね、dummy.Delete(); の部分で、なんとコピー元のオリジナルメールが消されてしまうんです(削除済みアイテムに保存された状態になる)

んなことあるわけないやん!って思うでしょ?
実際、私の環境じゃそんな現象は発生しません。

600人のユーザーのうち、10名のユーザーで再現するそうです。

いったいなぜそんなことが起こってしまうのか、いまのところ不明のままです。
んが。
#対策は、もちろんサイズの取得方法を変更しました。

2016年5月9日月曜日

Hyper-V で仮想マシンがインターネットにつながらない

実は長いこと Hyper-V で用意していた仮想マシンがネットワークにもインターネットにもつながってくれなくて、困り果てていました。

ホストマシンが開発環境、仮想マシンがテスト環境という構成でホストマシンで用意したファイルを仮想マシン側に提供する方法がなくてね。
唯一の方法が「メディア」で「DVDドライブ」を共有する方法。
ホストマシンにCD-Rをセットしてファイルを焼いて、仮想マシンで読み込む。
テスト用のファイルだし、CD-Rなもんだから使い捨てみたいになっちまって、ほんとに無駄でした。

どうにかならないものかと、いろいろと設定を繰り返していくうち、Hyper-Vの仮想「内部ネットワーク」スイッチを作ることで、ローカルネットワークのファイル共有まではできていました。


それだけだと、仮想マシンがインターネットに接続してくれません。
そのため Windows7とかのライセンス認証やアップデートが動作しないままになっていました。
(ライセンスが無効のまま起動される Windows7)

ようやく設定方法がわかりましたよ!

1.Hyper-V で内部スイッチを作成しておきます。
2.仮想マシンのネットワークには、内部スイッチを接続します。

ここまでは、OK。

3. ホストマシンのネットワーク設定で、「インターネット接続の共有」の共有先に内部スイッチを指定する!





これ、どこにも書いてなかったよ(探し方が悪かったのか?)
ようやく仮想マシンをネットワークにつなげることができるようになりました。

ときどきチャレンジしては失敗を繰り返していて、時間的には対して消化してないんだけど、この方法見つけるまでに、約1年かかってしまったwww

同じ悩みを持ってる方の参考になればいいな。
#どうやら初歩的なことのようで、だれも悩んでいるようではなかったのが不思議。

2017/11/06追記
実はその後も接続できなくなりまして、現在は VMWareを使っています。
VMWare、サクッと動いてくれたので気に入ってますw





2016年3月25日金曜日

Outlook addin で予定が消える!?

提供しているソフトウェアで、Outlookの予定表が消える!という問題が発生して調査しました。

手順は、こんなかんじ。
1)会議予定を作成して、参加者に会議開催通知をメールします。
2)予定をクリックして「会議のキャンセル」を選びます。(キャンセルメールが開きます)
3)AddinのItemSendイベント処理での会話結果から、Cancel=true;を設定してメールの送信を中止します。
4)キャンセルメールの右上「×」ボタンでメールを閉じます。
会議の予定が消えてなくなります。

新規にアドインを作成して
private void ThisAddIn_Startup(object sender, System.EventArgs e)
{ //ここを追加(1行)
  Application.ItemSend += new Outlook.ApplicationEvents_11_ItemSendEventHandler(onItemSend);
}

//以下を追加(12行。送信ボタン押下処理)
private void onItemSend(object Item, ref bool Cancel)
{
  Outlook.MeetingItem meetItem = Item as Outlook.MeetingItem;
  if(meetItem != null)
  {
    if (meetItem.MessageClass == "IPM.Schedule.Meeting.Canceled")
    {
      Cancel = true; // 会議開催通知をキャンセル
      return;
    }
  }
}
こんだけ書くだけで再現できました。
キャンセルの送信をキャンセルしているだけですね。

会議の主催者側のスケジュールが消えてしまい、受信者からは消せなくなってしまってとても不便な状態に陥ります。

・・・対策はいまのところ取れていません。
(会議キャンセルの ItemSend は無条件に送信するように、という対応しか取れませんね)


ちなみに。
通常のメールにエクセルのファイルを添付して、その状態で添付ファイルをダブルクリックして開いた状態でメールを送信すると、『ファイルが開かれているぞ!「はい」「いいえ」』という会話のメッセージボックスが出るんですが。
会議キャンセル通知のメールにエクセルファイルを添付して、ダブルクリックで開いた状態で送信すると、似たようなメッセージは表示されるものの、「いいえ」ボタンが表示されませんので必ず送信されます。(送信すると会議の予定は当然消えます)

こ子の動きから察するに、MSさんも勝手に消えてしまうことを把握していて無視してるんだな、と思えます。

かんべんしてくれーw