Skip to content

Instantly share code, notes, and snippets.

@seraphy
Last active February 10, 2018 01:25
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save seraphy/51b509592e0b20fc77612673d490f247 to your computer and use it in GitHub Desktop.
Save seraphy/51b509592e0b20fc77612673d490f247 to your computer and use it in GitHub Desktop.
C#によるRTDサーバーの実装例. Visual Studio 2017 Express for Desktopでビルド可能。=RTD("SeraRTDSvr",,"c:\temp\sample.txt","size") のようなRTD式をセルに入れると、指定したファイルのファイルサイズをリアルタイムで取得しつづける。そのほかにtime, listといったオプションがある。ビルドしたdllはCOMオブジェクトとしてregasmでレジストリに登録することでExcelからRTDで検索可能とする。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;
using System.Threading;
using System.Diagnostics;
using System.Windows.Forms;
using System.Threading.Tasks;
using System.IO;
/// <summary>
/// C#によるRTDサーバーの実装例.
/// Excelのセルに「=RTD("SeraRTDSvr",,"c:\temp\sample1.txt", "size")」のようなRTD関数を入れると、
/// リアルタイムで、指定したファイルサイズを取得して表示しつづける。
/// 第1引数にはファイルパス、第2引数には「time」または「size」を指定して、ファイルの更新日時またはファイルサイズを取得できる。
/// 第一引数にフォルダを指定した場合には、「list」を指定することでファイルの一覧をタブ区切りで取得することもできる。
/// </summary>
/// <remarks>
/// ビルド時には、アセンブリはCOM参照可能にしておくこと.
/// また、ビルド時には参照設定でWindowsFormが必要.
/// 生成したdllは、管理者権限で
/// regasm.exe /codebase SeraRTDServerExample.dll を実行してレジストリに登録する必要がある.
///
/// 参考にしたところ.
/// https://weblogs.asp.net/kennykerr/Rtd3
/// https://weblogs.asp.net/kennykerr/Rtd6
/// https://weblogs.asp.net/kennykerr/Rtd7
/// </remarks>
namespace SeraRTDServerExample
{
/// <summary>
/// RTDのイベントのコールバック用インターフェイス.
/// </summary>
[ComImport]
[TypeLibType(TypeLibTypeFlags.FDual | TypeLibTypeFlags.FDispatchable)]
[Guid("A43788C1-D91B-11D3-8F39-00C04F3651B8")]
public interface IRTDUpdateEvent
{
[DispId(10)]
void UpdateNotify();
[DispId(11)]
int HeartbeatInterval
{
get;
set;
}
[DispId(12)]
void Disconnect();
}
/// <summary>
/// Excelから呼び出されるRTDインターフェイス
/// </summary>
[ComImport]
[TypeLibType(TypeLibTypeFlags.FDual | TypeLibTypeFlags.FDispatchable)]
[Guid("EC0E6191-DB51-11D3-8F3E-00C04F3651B8")]
public interface IRtdServer
{
[DispId(10)]
int ServerStart(IRTDUpdateEvent callback);
[DispId(11)]
object ConnectData(int topicId,
[MarshalAs(UnmanagedType.SafeArray, SafeArraySubType = VarEnum.VT_VARIANT)] ref Array strings,
ref bool newValues);
[DispId(12)]
[return: MarshalAs(UnmanagedType.SafeArray, SafeArraySubType = VarEnum.VT_VARIANT)]
Array RefreshData(ref int topicCount);
[DispId(13)]
void DisconnectData(int topicId);
[DispId(14)]
int Heartbeat();
[DispId(15)]
void ServerTerminate();
}
/// <summary>
/// シンプルなRTDサーバーの実装例
/// </summary>
[Guid("5B6AA03F-F280-4B06-AFA3-519C9215D6BD")]
[ProgId("SeraRtdSvr")]
//[TypeLibType(TypeLibTypeFlags.FDual | TypeLibTypeFlags.FDispatchable)]
public class SeraphyRTDServer : IRtdServer
{
/// <summary>
/// TopicIdごとの値を保持し更新の有無を判定する値ホルダー
/// </summary>
class ValueHolder
{
/// <summary>
/// 値のリゾルバ
/// </summary>
private Func<object> resolver;
/// <summary>
/// 現在の値
/// </summary>
public object Current { get; set; }
/// <summary>
/// 更新の有無
/// </summary>
public bool Modified { set; get; }
/// <summary>
/// コンストラクタ
/// </summary>
/// <param name="resolver"></param>
internal ValueHolder(Func<object> resolver)
{
this.resolver = resolver;
}
/// <summary>
/// 更新する.
/// 変更がある場合は更新の有無が設定される.
/// </summary>
public void Refresh()
{
object value;
try
{
value = resolver();
}
catch (Exception ex)
{
Debug.Print("▼exception {0}", ex.Message);
// https://groups.google.com/forum/#!topic/exceldna/Z6mmxJ4LSbM
// ErrDiv0 = -2146826281
// ErrNA = -2146826246
// ErrName = -2146826259
// ErrNull = -2146826288
// ErrNum = -2146826252
// ErrRef = -2146826265
// ErrValue = -2146826273
value = new ErrorWrapper(-2146826246); // N/A
}
if (!Object.Equals(value, Current))
{
Modified = true; // ModifiedはOffにはしない
Debug.Print("◇ modified old={0}, new={1}", Current, value);
}
Current = value;
}
}
/// <summary>
/// タスクを終了させるためのキャンセラレーショントークンソース
/// </summary>
private CancellationTokenSource cancelTokenSource;
/// <summary>
/// 定期的にRTDにイベントを通知するための無限ループタスク
/// </summary>
private Task task;
/// <summary>
/// RTDイベントコールバック
/// </summary>
private IRTDUpdateEvent callback;
/// <summary>
/// RTDイベントコールバックを使う場合の同期コンテキスト
/// </summary>
private SynchronizationContext synchronizationContext;
/// <summary>
/// TopicIdごとのValueHolderを保持するマップ.
/// </summary>
private Dictionary<int, ValueHolder> TopicMap = new Dictionary<int, ValueHolder>();
/// <summary>
/// ファイル名に対する更新日付を取得する。例外の場合は例外メッセージを返す.
/// </summary>
/// <param name="name">ファイル名</param>
/// <returns>更新日付</returns>
private object GetLastWriteTime(string name)
{
return System.IO.File.GetLastWriteTime(name);
}
/// <summary>
/// ファイル名に対するファイルサイズを取得する。例外の場合は例外メッセージを返す.
/// </summary>
/// <param name="name">ファイル名</param>
/// <returns>更新日付</returns>
private object GetFileSize(string name)
{
return new System.IO.FileInfo(name).Length;
}
/// <summary>
/// ファイルまたはディレクトリに対するタブ区切りファイル一覧を返す.
/// </summary>
/// <remarks>
/// 本当は配列を返したいのだが、方法がなさそうなので受け取り側で分割する.
/// </remarks>
/// <param name="name">ファイル名</param>
/// <returns>更新日付</returns>
private object GetFileList(string name)
{
var attr = File.GetAttributes(name);
if (attr.HasFlag(FileAttributes.Directory))
{
var dir = new DirectoryInfo(name);
return String.Join("\t", dir.GetFiles().Select(f => f.FullName).ToArray());
}
return name;
}
/// <summary>
/// リアルタイム データ サーバーから新しいトピックを追加します。
/// ConnectData メソッドは、リアルタイム データ関数を含むファイルが開かれたとき、
/// またはユーザーが RTD 関数を含む新しい数式を入力したときに呼び出されます。
/// </summary>
/// <param name="topicId">必ず指定します。整数型 (Integer) の値を指定します。
/// トピックを識別する一意の値を指定します。この値は、Excel によって割り当てられます。</param>
/// <param name="strings">必ず指定します。オブジェクト型 (Object) の値を指定します。
/// トピックを識別する文字列の 1 次元配列を指定します。(RTD関数の引数)</param>
/// <param name="newValues">必ず指定します。ブール型 (Boolean) の値を指定します。
/// 入力値がFALSEの場合はRTDセルを含むブックをロードしたなどでセル上に既存の値があることを示します。
/// TRUE(非0)の場合はRTDセルを新規に入力したことを示します。
/// 返却値は、新規の値を使用するべきかを示し、FALSEの場合は既存のセルの値を変更しないことを示します。</param>
/// <returns></returns>
public object ConnectData(
int topicId,
[MarshalAs(UnmanagedType.SafeArray, SafeArraySubType = VarEnum.VT_VARIANT)] ref Array strings,
ref bool newValues)
{
PrintMethodInfo();
try
{
// RTD関数の引数(トピック)の取得
// トピックは1個以上、最大253個までありえる。
// (なお、同一のパラメータは同一のtopicidが割り当てられるので
// セルのコピー等を行った場合は本メソッドが重ねて呼び出されることはない。)
string fname = strings.GetValue(0).ToString();
string fmt = "time";
if (strings.Length >= 2) // 2個目以降は省略可
{
fmt = strings.GetValue(1).ToString();
}
Debug.Print("○ topicId={0} fname={1}, format={2}", topicId, fname, fmt);
Func<object> func;
switch (fmt)
{
case "time":
default:
func = () => this.GetLastWriteTime(fname);
break;
case "size":
func = () => this.GetFileSize(fname);
break;
case "list":
func = () => this.GetFileList(fname);
break;
}
// 割り当てられたTOPICIDと関連づけて保存する
var valueHolder = new ValueHolder(func);
TopicMap[topicId] = valueHolder;
// セルを更新することを示す
newValues = true;
// データを取得し返却する.
valueHolder.Refresh();
valueHolder.Modified = false;
return valueHolder.Current;
}
catch (Exception ex)
{
return ex.ToString();
}
}
/// <summary>
/// RTD (リアルタイム データ) サーバー アプリケーションに、トピックが使用されなくなったことを通知します。
/// </summary>
/// <param name="topicId">必ず指定します。整数型 (Integer) の値を指定します。
/// トピックに割り当てる一意の値を指定します。この値は、Excel によって割り当てられます。</param>
public void DisconnectData(int topicId)
{
PrintMethodInfo();
TopicMap.Remove(topicId);
}
/// <summary>
/// リアルタイム データ サーバーがアクティブかどうかを判別します。
/// </summary>
/// <returns>ゼロまたは負の値は失敗を示し、正の値は、サーバーがアクティブであることを示します。</returns>
public int Heartbeat()
{
// ハートビート通知
PrintMethodInfo();
return 1;
}
/// <summary>
/// このメソッドは、Excel が新しいデータを取得するときに呼び出されます。
/// </summary>
/// <param name="topicCount">必ず指定します。整数型 (Integer) の値を指定します。
/// RTD サーバーは、TopicCount の値を、取得された配列内の要素数に変更する必要があります。</param>
/// <returns>新しいデータが格納されたバリアント型 (Variant) の配列。</returns>
[return: MarshalAs(UnmanagedType.SafeArray, SafeArraySubType = VarEnum.VT_VARIANT)]
public Array RefreshData(ref int topicCount)
{
PrintMethodInfo();
// 更新されているアイテムの抽出
var modifiedItems = TopicMap.Where(entry => entry.Value.Modified).ToList();
int count = modifiedItems.Count; // アイテムの個数
object[,] data = new object[2, count]; // TOPICID, 値のペアからなる2次元配列
int index = 0;
foreach (var entry in modifiedItems)
{
int topicId = entry.Key;
object value = entry.Value.Current;
Debug.Print("* topidId={0}, value={1}", topicId, value);
data[0, index] = topicId; // topicId
data[1, index] = value; // 現在値
entry.Value.Modified = false; // 返却したアイテムはModifiedをクリアする
++index;
}
topicCount = count;
return data;
}
/// <summary>
/// ServerStart メソッドは、リアルタイム データ サーバーがインスタンス化された直後に呼び出されます。
/// </summary>
/// <param name="callback">必ず指定します。IRTDUpdateEvent オブジェクトを指定します。コールバック オブジェクトを指定します。</param>
/// <returns>負の値またはゼロはサーバーの起動に失敗したことを示し、正の値は起動が成功したことを示します。</returns>
public int ServerStart(IRTDUpdateEvent callback)
{
PrintMethodInfo();
if ((task == null || task.IsCompleted) && callback != null)
{
this.callback = callback;
// RTDイベントはSTAのため別スレッドから呼び出すためはマーシャリングする必要がある.
synchronizationContext = SynchronizationContext.Current ?? new WindowsFormsSynchronizationContext();
Debug.Print("synchronizationContext={0}", synchronizationContext);
// タイマー的な無限ループタスクを開始する.
cancelTokenSource = new CancellationTokenSource();
task = new Task(WatchLoop, cancelTokenSource.Token, TaskCreationOptions.LongRunning);
task.Start();
return 1;
}
return 0;
}
/// <summary>
/// キャンセルが指示されるまで現在保持しているTopicIdの状態を1秒ごとに検査し、
/// 更新があればRTDイベントコールバックでExcelに更新を通知する無限ループタスク
/// </summary>
private void WatchLoop()
{
// キャンセルされるまで無限ループする
while (!cancelTokenSource.Token.IsCancellationRequested)
{
if (!cancelTokenSource.Token.WaitHandle.WaitOne(1000)) // 1000mSec待ち
{
// タイムアウトした場合 = キャンセルされていない場合
PrintMethodInfo("Timeout");
if (UpdateAllTopic() > 0)
{
// Excelに更新を通知する.
synchronizationContext.Post(x => {
try
{
PrintMethodInfo("UpdateNotify");
callback?.UpdateNotify();
}
catch (Exception ex)
{
// コールバックに失敗した場合はExcelとの関係が異常になっている
Debug.Print("★Callback failed. " + ex.Message);
cancelTokenSource.Cancel();
}
}, null);
}
}
}
Debug.Print("★★end★★");
}
/// <summary>
/// 保持している、すべてのTopicの更新を行う
/// </summary>
/// <returns>更新があった個数を返す</returns>
private int UpdateAllTopic()
{
int modifiedCount = 0;
foreach (var valueHolder in TopicMap.Values)
{
valueHolder.Refresh();
modifiedCount += valueHolder.Modified ? 1 : 0;
}
return modifiedCount;
}
/// <summary>
/// リアルタイム データ サーバーへの接続を終了します。
/// </summary>
public void ServerTerminate()
{
PrintMethodInfo();
Dispose();
}
public SeraphyRTDServer()
{
PrintMethodInfo();
}
~SeraphyRTDServer()
{
PrintMethodInfo();
Dispose(); // ServerTerminate前にCOMがリリースされた場合の対応
}
/// <summary>
/// 後片付け。タスク等を終了させます.
/// </summary>
private void Dispose()
{
cancelTokenSource?.Cancel();
task?.Wait();
cancelTokenSource = null;
task = null;
synchronizationContext = null;
}
/// <summary>
/// 診断メッセージ表示
/// </summary>
/// <param name="member">関数名等。省略された場合は呼び元のメソッド名が自動的に適用される</param>
private void PrintMethodInfo([System.Runtime.CompilerServices.CallerMemberName] string member = "")
{
Debug.Print("☆{0}: @{1:X} thread={3}({2})", member, GetHashCode(),
Thread.CurrentThread.GetApartmentState(), Thread.CurrentThread.ManagedThreadId);
}
}
}
[12020] ☆.ctor: @378734A thread=1(STA)
[12020] ☆ServerStart: @378734A thread=1(STA)
[12020] synchronizationContext=System.Windows.Forms.WindowsFormsSynchronizationContext
[12020] ☆ConnectData: @378734A thread=1(STA)
[12020] ○ topicId=0 fname=c:\temp\testdata\sample2.txt, format=size
[12020] ◇ modified old=, new=4
[12020] ☆ConnectData: @378734A thread=1(STA)
[12020] ○ topicId=1 fname=c:\temp\testdata\sample1.txt, format=size
[12020] ◇ modified old=, new=14
[12020] ☆ConnectData: @378734A thread=1(STA)
[12020] ○ topicId=2 fname=c:\temp\testdata, format=list
[12020] ◇ modified old=, new=c:\temp\testdata\sample1.txt c:\temp\testdata\sample2.txt
[12020] ☆ConnectData: @378734A thread=1(STA)
[12020] ○ topicId=3 fname=, format=size
[12020] ◇ modified old=, new=パスの形式が無効です。
[12020] ☆Timeout: @378734A thread=4(MTA)
[12020] ☆Timeout: @378734A thread=4(MTA)
[12020] ☆Timeout: @378734A thread=4(MTA)
[12020] ☆Timeout: @378734A thread=4(MTA)
[12020] ◇ modified old=14, new=5
[12020] ☆UpdateNotify: @378734A thread=1(STA)
[12020] ☆RefreshData: @378734A thread=1(STA)
[12020] * topidId=1, value=5
[12020] ☆Timeout: @378734A thread=4(MTA)
[12020] ☆Timeout: @378734A thread=4(MTA)
[12020] ☆Timeout: @378734A thread=4(MTA)
[12020] ☆Timeout: @378734A thread=4(MTA)
[12020] ☆Timeout: @378734A thread=4(MTA)
[12020] ☆ServerTerminate: @378734A thread=1(STA)
[12020] ★★end★★
[12020] ☆Finalize: @378734A thread=2(MTA)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment