ラベル C# の投稿を表示しています。 すべての投稿を表示
ラベル C# の投稿を表示しています。 すべての投稿を表示

2024年11月17日日曜日

SQLiteを使ったプログラムで Out of memory 例外が発生する

メモリいっぱいあるのに、Out of memory

C# で開発したファイル処理プログラム。
SQLite を使ってインデックスを貼り高速検索するようにしてます。
SQLite の便利な機能に、データベースをメモリ内で処理させるってのがあります。

   SqlConnectionSb = new SQLiteConnectionStringBuilder { DataSource = ":memory:" };
   SqlConnection = new SQLiteConnection(SqlConnectionSb.ToString());
  
こうすることで、オンメモリ処理になり、さくさく動くようになります。
DataSource をハードディスク上のファイルに置き換えることもできますが、それだと遅くて使い物にならないような機能のプログラムなんです。
このプログラムにちょっとばかり大きなファイルを処理させると、途中で Out of memory 例外でエラーになって落ちてしまう問題が出ました。
どーして?
タスクマネージャで見ててもメモリの空き容量は十分にあるのに。
そんな、ばなな。

キャッシュを大きくしてみたり

SQLite out of memory で検索しても、どういうわけかなにもヒットするページがありません。
しかたなく接続時のパラメータを変更してみたり。

SqlConnectionSb = new SQLiteConnectionStringBuilder { DataSource = ":memory:", CacheSize = 10000 };
PageSize とか MaxPageCount といったパラメータも変更してみたけど、効果なし・・・困りました。

32ビットを選ぶ?

Visual Studio のプロジェクトは「AnyCPU」としていて、32bitでも64bitでも動作するようにしてるつもりだったけど。
プロジェクトプロパティ「ビルド」の項目に、「✅32ビットを選ぶ」ってのがありました。

なんじゃ、こりゃあ?ってなくらい、知りませんでしたよ。(笑)
このオプションのせいで、AnyCPU が 32bit プロセスとしてビルドされてしまうわけです。
これまでリリースしてたプロダクト、全部再確認が必要になりましたよw

上記チェックボックスをOFFにして、64bitOSで動作させたら、Out of memory例外は出なくなりました。
あ、プロジェクトプロパティは Debug と Release の両方の構成を変えなきゃならないから、注意してくださいまし。

今回の問題は、問題を再現するのにもトライアンドエラーするのにも1時間以上かかるので、大変でした。

ま、解決できてよかった。
めでたしめでたし。

2023年6月2日金曜日

Windows hosts ファイルを編集して名前解決させる。

hosts

Virtual Machine で開発することが圧倒的に多くなってきましたね。
本番環境と開発環境を分けるためにドメインの名前解決を一時的に変更することも多々あります。
そこで必要になるのが hosts の編集です。

hosts を編集して名前解決させるには

これが、なかなかめんどくさいw
1.メモ帳を管理者モードで起動
2.ファイルを開くで、右下のフィルタを「すべてのファイル」に変更。
3.C:\Windows\System32\drivers\etc\hosts を開いて編集。
4.保存。
5.コマンドプロンプトを開いて
6.C:\> ipconfig /flushdns

一日一回とかならまだいいけど、数回実施するとなるとめんどくさいね。
ということで、プログラム書いてみましたw

HostsEditor

今回は、Visual Studio 2022 Comunity の C# Windows Form .NET プロジェクトを作成しました。
Form1 には、保存終了用のツールボタンと、全画面に Dock した TextBox を配置します
TextBox のプロパティは、Multiline=true, MaxLength を大きめに。font も大きめに、程度かな。
Name は editText と付けておきました。

Form1.cs

プログラムは、こんな感じです。
hosts ファイルの位置を保持
全行を読み込んで TextBox に入れて編集可能にする
保存ボタンが押されたら、保存して、CMD.exe で ipconfig /flushdns を実行。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace HostsEditor
{
    public partial class Form1 : Form
    {
        internal string HostsFileName { get; set; }
        public Form1()
        {
            InitializeComponent();
        }

        //初期表示
        private void Form1_Load(object sender, EventArgs e)
        {
            // ファイル名
            string system_folder = Environment.GetFolderPath(Environment.SpecialFolder.System);
            string hosts_path = Path.Combine(system_folder, "drivers", "etc");
            HostsFileName = Path.Combine(hosts_path, "hosts");

            System.IO.FileInfo fi = new System.IO.FileInfo(HostsFileName);
            if(4194304 < fi.Length)
            {
                MessageBox.Show("hosts ファイルが大きすぎます。他の手段で編集してください。",
                    "エラー", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
                Close();
            }
            // ファイルを 65,536 byte まで読み込む
            string[] lines = File.ReadAllLines(HostsFileName);
            string text = String.Join("\r\n", lines);
            editText.Text = text;
            // 全選択を解除
            editText.SelectionStart = 0;
        }

        private void closeClick(object sender, EventArgs e)
        {
            try
            {
                // 保存
                string text = editText.Text;
                using (StreamWriter streamWriter = new StreamWriter(HostsFileName))
                {
                    // Writeメソッドで文字列データを書き込む
                    streamWriter.Write(text);
                    // StreamWriterオブジェクトを閉じる
                    streamWriter.Close();
                }
                // ipconfig /flushdns を実行。完了まで待つ。
                System.Diagnostics.Process process = new System.Diagnostics.Process();
                //ComSpec(cmd.exe)のパスを取得して、FileNameプロパティに指定
                process.StartInfo.FileName = System.Environment.GetEnvironmentVariable("ComSpec");
                //出力を読み取れるようにする
                process.StartInfo.UseShellExecute = false;
                process.StartInfo.RedirectStandardOutput = true;
                process.StartInfo.RedirectStandardInput = false;
                //ウィンドウを表示しないようにする
                process.StartInfo.CreateNoWindow = true;
                //コマンドラインを指定("/c"は実行後閉じるために必要)
                process.StartInfo.Arguments = @"/c ipconfig /flushdns";
                //起動
                process.Start();
                //(親プロセス、子プロセスでブロック防止のため)
                process.WaitForExit();
                process.Close();
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message, "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
            // 画面を閉じる
            Close();
        }
    }
}

管理者として起動

プログラムを管理者として起動する必要があるので、Program.cs を編集します。
Main部分に以下の内容を追記します。

        static void Main(string[] args)
        {

            // 管理者権限に昇格させて自分自身を起動する
#if (!DEBUG)
            Thread.GetDomain().SetPrincipalPolicy(PrincipalPolicy.WindowsPrincipal);
            var pri = (WindowsPrincipal)Thread.CurrentPrincipal;
            //管理者権限以外での起動なら, 別プロセスで本アプリを起動する
            if (!pri.IsInRole(WindowsBuiltInRole.Administrator))
            {
                var proc = new ProcessStartInfo()
                {
                    WorkingDirectory = Environment.CurrentDirectory,
                    FileName = Assembly.GetEntryAssembly().Location,
                    Verb = "RunAs"
                };

                if (args.Length >= 1)
                    proc.Arguments = string.Join(" ", args);

                //別プロセスで本アプリを起動する
                Process.Start(proc);

                //現在プロセス終了
                return;
            }
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new Form1());
#endif
        }

まとめ

DOBON.NET さんの記事を参照しました。
デバッグ時は、VisualStudio を管理者モードで起動する必要があります。
わりと、楽になりましたw
めでたしめでたし。

2023年3月28日火曜日

PowerShell で Mark of The Web 属性を付加するとエラーになる?

Mark of The Web

Mark of The Web (略して MoTW)は Windows のファイル拡張属性で、インターネットからダウンロードされたファイルに特別なしるしを付けて警告を促せるようにするものです。
メールソフト用のプラグインを作っていますので、この属性を使用するのは至極当然のことで。
というわけで、弊社の製品にも実装しました。

問題に遭遇

例によって、いろいろな問題に遭遇したのですが、特に顕著な問題について、ここで報告しておきます。
C:\Temp フォルダに2つのテキストファイルを置きます。(中身はなんでもいいです)
ファイル名は Test_(1.txt, Test_(2.txt です。

まず、PowerShell を起動して(*0)、以下を実行してみます。

PS C:\Temp> Set-Content -Path "C:\Temp\test_(1.txt" -Stream Zone.Identifier -Encoding oem -NoNewline -Value "[ZoneTransfer]`r`nZoneId=3"

エクスプローラで、プロパティを参照すると、セキュリティ属性が付加されているのがわかります。

プログラムから呼び出してみる

次に、対象ファイルを Test_(2.txt に変えて、同じ動作をするプログラムを実装して、実行してみます。
使用したのは VisualStudio 2022, .NET 4.8 ターゲットの C# コンソールアプリケーションです。
using 行を端折ってますが、デフォルトのものでかまいません。

namespace MoTW
{
    class Program
    {

        static void Main(string[] args)
        {
            string file2 = @"C:\Temp\test_(2.txt";
            setMoTW_byPowerShell(file2);
            Console.ReadLine();
        }

        // set MoTW by PowerShell
        private static bool setMoTW_byPowerShell(string filename)
        {
            bool rc = false;
            string arg_format = "Set-Content -Path \"{0}\" -Stream Zone.Identifier -Encoding oem -NoNewline -Value \"[ZoneTransfer]`r`nZoneId=3\"";
            try
            {
                var psinfo = new ProcessStartInfo();
                psinfo.FileName = "powershell.exe";
                psinfo.UseShellExecute = true;
                psinfo.Arguments = string.Format(arg_format, filename);
                psinfo.CreateNoWindow = true;
                psinfo.WindowStyle = ProcessWindowStyle.Hidden;
                Process proc = Process.Start(psinfo);
                proc.WaitForExit();
                rc = (proc.ExitCode == 0);
            }
            catch (Exception e)
            {
                Console.WriteLine(e.Message);
                rc = false;
            }
            return rc;
        }
    }
}

Power Shell、普段使わないので、改行の指定方法が '`r`n' だということも今回調べましたよw
で、結果。エクスプローラで Test_(2.txt のプロパティを見るとですね。
MoTW 付いていないんですね、これが。
実際に作ったのは、アドインソフトウェアなので DLL ファイルなんですが、同じように MoTW 付いてくれなくて、本当に困ってしまいました。
ちなみに、ファイル名が Test1.txt, Test2.txt であれば、PowerShell も C# プログラムも正常に MoTW が付加されます。

つまり、'(' の文字が含まれていると、実行プログラムからの呼び出しではうまくいかないんですよ。

メールソフト用のアドインでは

1.添付ファイルをディスク上に保存する。
2.ShellExecuteEx を使って(*1)、保存したファイルを開く。
という動作を実装しているので、MoTW 属性のついていないファイルがそのまま開かれたり実行されてしまうんですね。
ZIP書庫を展開したファイルが実行ファイルだった場合、無条件に実行されてしまうことになってしまいます。
危険な状態です。
とても MoTW 対応です!とは言えませんねw

まとめ

結局、各種記号のいくつかが含まれるファイル名では失敗することがわかったので

1.ファイル名を Base64Url エンコードした名前に変更する。
2.PowerShell で Set-Content する(上記のコード)。
3.ファイル名を Base64Url デコードする。
という、本来やらなくてもいいような処理を実装することで問題を回避できました。
めでたしめでたし。(*2)

*0

MoTW を付加する方法には、「デバイスパス指定子」を使って FileStream を操作し Zone識別ファイルを作成する方法もあります。
わんくま同盟の掲示板で魔界の仮面弁士さんに教えていただきました。
この方法は、.NET 4.6.2 以上でうまくいくのですが、Outlook 用のアドインを .NET 4.6.2 以上で構築してもDLL内では .NET4.6.2以前の場合と同じようにエラーとなってしまうため、利用を断念しました。
Outlook がなにかやらかしてるのだと思います。

*1

保存したファイルを開く処理で、System.Diagnostics.Process を使って Start() すると、MoTW を無視して実行してしまいます。
んで、MoTW 属性も消されてしまいます。
しかたなく WIN32API の ShellExecuteEx で Mask 値を SEE_MASK_NOZONECHECKS がセットされないようにして呼び出すようにしました。

*2

「めでたしめでたし」じゃないんですよ。
多くのウィルスソフトやマルウェアなどがメールを介して感染しています。
弊社のプログラムはこれで問題を解決できましたが、同様の処理を実装しているメール用アドインなんかでは致命的です。

とっても致命的な問題だと思ったので、マイクロソフトに報告しておきました。

2020年11月28日土曜日

「マネージとネイティブの境界の前でハンドルされませんでした」という例外でハマった。

 開発中のDLLをデバッグしているときに、「型 'System.Runtime.InteropServices.COMException' の例外が XXX.dll で発生しましたが、マネージとネイティブの境界の前でハンドルされませんでした」
という例外が出るようになって、非常に困ったので、まとめ。



例外が出るのはいいとして、

□この種類の例外がスローされると中断します
のチェックボックスをOFFにしても、必ず中断されてしまうから困ったのです。

しかも、ですよ。
catch(Exception e){}のブロックに入ってくれない。
最もひどい状態では、同じ部分で延々と例外のダイアログが表示され続けるために
「デバッグの中止」
を使ってプロセスを落とすしかなくなるんです。

ちなみに開発環境は Visual Studio 2015 です。
開発しているのは、Office Outlook 用のアドインDLL
メールのヘッダに独自に設定している拡張ヘッダがあるかどうかを調べるコードです。
一般のメールにはそのヘッダはないので、「そんなヘッダはありません」という例外が出ます。(それはいい)
try{}catch{}ブロックでその例外を無視すればいいだけのコードです。

まず、「catch(Exception e){}のブロックに入ってくれない。」件に関しては、プロジェクトのProperies 設定の「デバッグ」で
□ネイティブコードのデバッグを有効にする
をチェックすることで、catch{}ブロックに入ってくるようになりました。


しか~し。
□この種類の例外がスローされると中断します
のチェックボックスをOFFにしても必ず例外で中断する現象は変わりません。

開発環境によってはこの問題が発生しないので、上手くいく環境といかない環境を比較しながら調べてみました。
結論:「ツール」-「オプション」-「デバッグ」で
□例外が AppDomain またはマネージ/ネイティブの境界を超える場合にブレークする(マネージのみ)
を OFF にします。



以上で例外での中断がなくなりました。
この問題の解決にもけっこうな日数かかりました。orz

2017年12月21日木曜日

SharpZipLibでZIP書庫ファイルをZIP書庫にするとエラー?

SharpZipLibを使ってファイルをZIP書庫に圧縮するプログラムで、ZIP書庫を対象にするとうまくいっていないことが判明して調査してみました。
通常の疑似コードだとこんな感じ。
string fromFile = @"c:\test.txt"; //圧縮する元のファイル
string toFile = @"c:\test.zip"; //圧縮ファイル名
FastZip.CreateZip(toFile, work_path, false, "test.txt", null);

これで元ファイルがZIP書庫だと
fromFile = @"c:\test.zip";
toFile = @"c:\test.zip";
となってしまい、読み込み元と書き込み先が同じになってしまいます。

当然回避するようにしていました。(以下疑似コード)
if(fromFile == toFile)
{
  toFile=fromFile + ".t";
}
ファイル名の末尾にさらに .t を追加して対応。
圧縮後ファイル名を変更するようにしておりました。

しかーし。実際は「toFile は他のプロセスが使用中」という例外が出ていました。
見た目、同じファイルが残るので気づかなかった(笑

".t"を"_t"とか".zip"とかにしても同様で。
やむなく、一時的なファイル名(時刻と秒を文字列化した一時ファイル名)を作成して処理するように変更しました。

要するに、SharpZipLib 使う際、圧縮先のファイル名には似たような名前が使えない(らしい)ということのようです。

と、いうことで Outlook用の暗号化添付プラグインをバージョンアップしました。
こちらから最新版をダウンロードできます。
https://seasoft.co.jp/products/BizAttacher.html
無料でも使えます。1か月お試し運用もできます!

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

2015年8月31日月曜日

【C#】 仮想ListView で頻繁に RetrieveVirtualItem が呼び出される

C# の Windows フォーム で ListView を仮想モードで動作させると、リストビュー上にマウスカーソルがあるだけで、頻繁に RetrieveVirtualItem が呼び出される、という現象がでます。

初期表示状態では、落ち着いていてもソートやスクロールをした後だと、どうも表示範囲の再表示が完了したことをわかってないみたいで、マウスがじっとしていても何度も何度も呼び出されてしまい、性能の低いPCだと、CPUの使用率が100%になったり、どうかすると真っ白になって固まってしまいます。
(ListViewItem のリストをキャッシュしていても、です)

ネット上にListView のソースらしきものが落ちていましたので、眺めてみましたところ・・・

たとえば、MouseHover の部分を見ると、

        protected override void OnMouseHover(EventArgs e)  {
            ListViewItem item = null;
            if (this.Items.Count > 0) {
                Point pos = Cursor.Position;
                pos = PointToClientInternal(pos);
                item = GetItemAt(pos.X, pos.Y);
            }
 
という記述があり、必要かどうかを判断することなくマウス位置の要素を取り出してます。
ということで、ListView の派生クラスをこさえて、MouseHover を処理しないという暴挙wを働いてみました。

    public class SsListView : System.Windows.Forms.ListView
    {
        public SsListView()
            : base()
        {
        }
        protected override void OnMouseHover(EventArgs e)
        {
             // base.OnMouseHover(e);
        }
   }

デザイナーで読み込む方法を知らないので、フォームのコンストラクタで上記の派生クラスを張り付けた状態で動作させると、マウスが静止しているときには GetItemAt が呼ばれなくなり、CPUの占有がなくなりました。
※この方法では、リスト項目のツールチップが表示されなくなってしまいますので注意。

でも、リストビュー上でマウスを動かし続けると、相変わらず更新され続けてしまいます。
そこで、派生クラスに次のコードを追加してみました。

         const int WM_MOUSEMOVE = 0x0200;
        [SecurityPermissionAttribute(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode)]
        [SecurityPermissionAttribute(SecurityAction.InheritanceDemand, Flags = SecurityPermissionFlag.UnmanagedCode)]
        protected override void WndProc(ref Message msg)
        {
            switch (msg.Msg)
            {
                case WM_MOUSEMOVE:
                    if ((Control.MouseButtons & MouseButtons.Left) != MouseButtons.Left)
                    {
                        return; // 無視させる
                    }
                    break;

            }
            base.WndProc(ref msg);
            return;
        }

ドラッグ処理中じゃなければ、無視するようにしてみたわけです。
これで、マウスをいくら動かしてもCPUを占有しなくなりましたよ。

何らかの弊害があるのかもしれませんが、とりあえずうまくいくようになったみたい・・・

本件について別の情報を入手しましたので追記しておきます。(まだ試してないw)
https://stackoverflow.com/questions/938896/flickering-in-listview-with-ownerdraw-and-virtualmode