2022/06/06

Razer Pro Click Mini

ゲーミングマウスの性能で高速スクロール可能なモバイルマウスがほしいという願望がずっとあって、サブに使っていたMX AnyWhere 2Sの買い替えを機に、Razer Pro Click Miniを買ってきた。

エクステリアデザインはエクセレント。Logicoolのモバイルマウスの最上位ラインはちょこちょこデザインを変えてきているが、「こういうのでいいんだよ」をとてもきれいにまとめた感じ。一応チェックした大きい方のRazer Pro Clickは縁にクロームのラインをあしらっているが、別にそういうのは要らない。

製品としては明らかにMX AnyWhere 3の対抗馬で、バッテリーが固定内蔵でないのが違うが、重量は単3一本の状態で実測でほぼ同じ(4g軽い)なので、使用上は大きな違いはない。単4一本にすればさらに軽くできる。
  • Pro Click Mini: 92g(単3一本)
  • Pro Click Mini: 81g(単4一本アダプター込み)
  • MX AnyWhere 3: 96g

ちなみに、上面蓋をどう固定しているのかと思ったら、本体の最後部にマグネットが仕込んであって、それで蓋のネジを引き付ける仕組みになっていた。したがって、爪が折れたりする心配とは無縁だが、机から落としたりすると簡単に外れて飛んでいくので、それはそれで注意が必要かも。

追随性とかは自分には正直差が分からないので、確認点はクリック音とホイールの回転。

クリック音


Pro Click Miniはクリック音が小さいのを売りの一つにしているが、確かに小さい。擬音的には、「カチッ」というより、小さく「ポクッ」という感じで、音が小さくかつ低音なので響かない。まあこれは、むしろLogicoolがMX AnyWhereのラインで重視していないのが謎な点ではある。

ホイール


まずフリーホイールとの切り替えはホイール手前のシーソースイッチで行うが、これが少し硬い。ただ、自分は常にフリーホイールで使うので(TrackPointで鍛えられた人間なので、ノッチに頼らなくても支障はない)、むしろ勝手に変わらなくてよい。

ホイールにはラバーに四角錐が5個並んだ滑り止めが施されている。

肝心のホイールの回転については、MX AnyWhere 3の極まったホイール(無音・無抵抗で、ブレもなく、とてもよく回る)に比べると、回転音がしてかすかにブレがある分、少し劣ると言わざるを得ない。シフトスイッチのあるMX AnyWhere 2Sよりも劣るので、この点はLogicoolに一日の長があるのだろうと思う。

ただ、これは極まったMX Anywhere 3と比べての話で、実用上の問題はない。

まとめ


結論としては、クリック音の小ささではPro Click Miniに、ホイールの回転ではMX AnyWhere 3に軍配が上がる。どちらも使い倒すけど。

2022/06/03

.NET 5でのアプリの更新

Microsoftストアで公開したアプリはストアのAPIを使って更新できますが、.NET 5以降は変わった点があるので、メモしておきます。 更新の際に出るダイアログのために、このオーナーとなるWindowを先に登録する必要がありますが、これは.NET 5より以前は以下のようなものでした。
[ComImport]
[Guid("3E68D4BD-7135-4D10-8018-9FB6D9F33FA1")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IInitializeWithWindow
{
void Initialize(IntPtr hwnd);
}
public void SetOwnerWindow(StoreContext context, Window window)
{
var handle = new WindowInteropHelper(window).Handle;
var initWindow = (IInitializeWithWindow)(object)context;
initWindow.Initialize(handle);
}
見てのとおり、COMのIInitializeWithWindowを定義しておいて、StoreContextのインスタンスをこれにキャストするものですが、.NET 5以降はInvalidCastExceptionが出て実行できなくなります。

このための修正としては、usingにWinRTを加えた上でAsでIInitializeWithWindowキャストする。
// using WinRT;
public void SetOwnerWindow(StoreContext context, Window window)
{
var handle = new WindowInteropHelper(window).Handle;
var initWindow = context.As<IInitializeWithWindow>();
initWindow.Initialize(handle);
}
もしくは、WinRT.Interop.InitializeWithWindow.Initializeを使う。これが公式に出ている方法で、StoreContextをキャストする必要がなく、したがってCOMの定義も要らなくなるので、やるならこちらだと思います。
// using WinRT.Interop;
public void SetOwnerWindow(StoreContext context, Window window)
{
var handle = new WindowInteropHelper(window).Handle;
InitializeWithWindow.Initialize(context, handle);
}
これを含めた更新のためのヘルパークラスの全体は以下のようになります。
using System;
using System.Net.NetworkInformation;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Interop;
using Windows.Services.Store;
using WinRT.Interop;
internal static class StoreHelper
{
/// <summary>
/// Checks if updated packages are available.
/// </summary>
/// <returns>True if available</returns>
/// <remarks>
/// If the packages are installed locally or published but as Package flights, this method
/// will not work correctly.
/// </remarks>
public static async Task<bool> CheckUpdateAsync()
{
if (!NetworkInterface.GetIsNetworkAvailable())
return false;
var context = StoreContext.GetDefault();
try
{
var updates = await context.GetAppAndOptionalStorePackageUpdatesAsync();
return (updates.Count > 0);
}
catch
{
return false;
}
}
/// <summary>
/// Proceeds to download and install updated packages.
/// </summary>
/// <param name="window">Owner window</param>
/// <returns>True if successfully finished downloading and installing</returns>
public static async Task<bool> ProceedUpdateAsync(Window window)
{
if (window is null)
throw new ArgumentNullException(nameof(window));
if (!NetworkInterface.GetIsNetworkAvailable())
return false;
var context = StoreContext.GetDefault();
try
{
var updates = await context.GetAppAndOptionalStorePackageUpdatesAsync();
if (updates.Count == 0)
return false;
SetOwnerWindow(context, window);
var result = await context.RequestDownloadAndInstallStorePackageUpdatesAsync(updates);
return (result.OverallState == StorePackageUpdateState.Completed);
}
catch
{
return false;
}
}
private static void SetOwnerWindow(StoreContext context, Window window)
{
var handle = new WindowInteropHelper(window).Handle;
InitializeWithWindow.Initialize(context, handle);
}
}
view raw StoreHelper.cs hosted with ❤ by GitHub