.NET 3.5+ 內建 TimeZoneInfo 類別,提供豐富的時區轉換、夏令時間判斷功能。但在 .NET 2.0 環境 (或維護舊專案) 時,僅能存取 TimeZone.CurrentTimeZone (僅限伺服器本地時區)。本篇示範如何透過 登錄機碼 讀取 Windows 完整時區資料庫,自行實作 TimeZoneInfo 核心功能:列舉所有时区、計算 UTC 偏移、判斷夏令時間、轉換時間。
所有時區資訊儲存在登錄機碼:HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones
每個子機碼 (如 "Taipei Standard Time") 包含:
Display:顯示名稱 (如 "(UTC+08:00) 台北")Std:標準時間名稱 (如 "台北標準時間")Dlt:夏令時間名稱 (如 "台北夏令時間")TZI:REG_BINARY 結構體,含基準偏移、夏令規則 (起始/結束月份、週、日、時)Dynamic DST:歷年夏令規則變更記錄 (如 2010 年前後規則不同)using System;
using System.Runtime.InteropServices;
[StructLayout(LayoutKind.Sequential)]
public struct SYSTEMTIME
{
public ushort wYear, wMonth, wDayOfWeek, wDay;
public ushort wHour, wMinute, wSecond, wMilliseconds;
public void SetFromBytes(byte[] data)
{
if (data.Length != 16) throw new ArgumentException("SYSTEMTIME 需 16 bytes");
wYear = BitConverter.ToUInt16(data, 0);
wMonth = BitConverter.ToUInt16(data, 2);
wDayOfWeek = BitConverter.ToUInt16(data, 4);
wDay = BitConverter.ToUInt16(data, 6);
wHour = BitConverter.ToUInt16(data, 8);
wMinute = BitConverter.ToUInt16(data, 10);
wSecond = BitConverter.ToUInt16(data, 12);
wMilliseconds = BitConverter.ToUInt16(data, 14);
}
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct TIME_ZONE_INFORMATION
{
public int Bias; // 基準偏移 (分鐘),UTC = Local + Bias
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
public string StandardName; // 標準時間名稱
public SYSTEMTIME StandardDate; // 標準時間生效規則
public int StandardBias; // 標準時間額外偏移
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
public string DaylightName; // 夏令時間名稱
public SYSTEMTIME DaylightDate; // 夏令時間生效規則
public int DaylightBias; // 夏令時間額外偏移 (通常 -60 分鐘)
public void SetFromBytes(byte[] data)
{
if (data.Length != 44) throw new ArgumentException("TZI 需 44 bytes");
Bias = BitConverter.ToInt32(data, 0);
StandardBias = BitConverter.ToInt32(data, 4);
DaylightBias = BitConverter.ToInt32(data, 8);
var buf = new byte[16];
Buffer.BlockCopy(data, 12, buf, 0, 16); StandardDate.SetFromBytes(buf);
Buffer.BlockCopy(data, 28, buf, 0, 16); DaylightDate.SetFromBytes(buf);
}
}
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.IO;
using Microsoft.Win32;
public class TimeZoneInfo
{
// --- 私有欄位 ---
private string _id; // 登錄機碼名稱 (如 "Taipei Standard Time")
private string _displayName; // 顯示名稱
private TIME_ZONE_INFORMATION _tzi; // 解析後的 TZI 結構
private ReadOnlyCollection _adjustmentRules;
// --- 公開屬性 ---
public string Id => _id;
public string DisplayName => _displayName;
public string StandardName => _tzi.StandardName;
public string DaylightName => _tzi.DaylightName;
public TimeSpan BaseUtcOffset => TimeSpan.FromMinutes(-_tzi.Bias); // 注號轉換
// --- 靜態工廠方法:列舉所有系統時區 ---
public static TimeZoneInfo[] GetSystemTimeZones()
{
var list = new List();
using (var rootKey = Registry.LocalMachine.OpenSubKey(
@"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones", false))
{
if (rootKey == null) throw new InvalidOperationException("找不到時區登錄機碼");
foreach (string zoneId in rootKey.GetSubKeyNames())
{
try
{
var tzi = new TimeZoneInfo { _id = zoneId };
tzi.LoadFromRegistry();
list.Add(tzi);
}
catch { /* 忽略損毀的時區項目 */ }
}
}
// 依 BaseUtcOffset 排序
list.Sort((a, b) => a.BaseUtcOffset.CompareTo(b.BaseUtcOffset));
return list.ToArray();
}
// --- 從登錄機碼載入資料 ---
private void LoadFromRegistry()
{
using (var key = Registry.LocalMachine.OpenSubKey(
$@"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones\{_id}", false))
{
if (key == null) throw new ArgumentException($"未知時區: {_id}");
_displayName = key.GetValue("Display")?.ToString() ?? _id;
_tzi.StandardName = key.GetValue("Std")?.ToString() ?? "";
_tzi.DaylightName = key.GetValue("Dlt")?.ToString() ?? "";
_tzi.SetFromBytes((byte[])key.GetValue("TZI"));
// 解析 Dynamic DST 歷年規則 (可選,建構 AdjustmentRule)
BuildAdjustmentRules(key);
}
}
// --- 建構 AdjustmentRule 集合 (支援歷年規則變更) ---
private void BuildAdjustmentRules(RegistryKey zoneKey)
{
var rules = new List();
using (var dstKey = zoneKey.OpenSubKey("Dynamic DST", false))
{
if (dstKey == null)
{
// 無歷年變更,使用單一規則
rules.Add(CreateAdjustmentRule(_tzi.DaylightDate, _tzi.StandardDate, _tzi.DaylightBias));
}
else
{
// 依年份建立多個規則
foreach (string yearStr in dstKey.GetValueNames())
{
if (int.TryParse(yearStr, out int year))
{
var tziBytes = (byte[])dstKey.GetValue(yearStr);
var tzi = new TIME_ZONE_INFORMATION();
tzi.SetFromBytes(tziBytes);
rules.Add(CreateAdjustmentRule(tzi.DaylightDate, tzi.StandardDate, tzi.DaylightBias, year));
}
}
}
}
_adjustmentRules = new ReadOnlyCollection(rules);
}
private AdjustmentRule CreateAdjustmentRule(SYSTEMTIME daylightStart, SYSTEMTIME standardStart, int daylightBias, int? startYear = null)
{
// 將 SYSTEMTIME 規則轉換為 DateTime 範圍
// 此處簡化:實際需依規則計算具體生效日期
var delta = TimeSpan.FromMinutes(-daylightBias); // 夏令偏移量 (通常 +1 小時)
return AdjustmentRule.CreateAdjustmentRule(
DateTime.MinValue.Date, DateTime.MaxValue.Date, delta,
CreateTransitionTime(daylightStart), CreateTransitionTime(standardStart));
}
private TransitionTime CreateTransitionTime(SYSTEMTIME st)
{
// SYSTEMTIME: wMonth=月, wDay=第幾週(1-5), wDayOfWeek=星期幾(0=日)
if (st.wMonth == 0) return TransitionTime.CreateFixedDateRule(new DateTime(1,1,1, st.wHour, st.wMinute, 0), 1, 1);
return TransitionTime.CreateFloatingDateRule(
new DateTime(1, 1, 1, st.wHour, st.wMinute, 0), st.wMonth, (int)st.wDay, (DayOfWeek)st.wDayOfWeek);
}
// --- 核心功能方法 ---
/// 判斷指定時間是否在夏令時間
public bool IsDaylightSavingTime(DateTime dateTime)
{
// 簡化:依 AdjustmentRule 判斷
foreach (var rule in _adjustmentRules)
{
if (rule.DateStart <= dateTime && dateTime < rule.DateEnd)
{
var dstStart = rule.DaylightTransitionStart;
var dstEnd = rule.DaylightTransitionEnd;
// 實際需計算該年具體轉換日期時間比較
return true; // 此處略過詳細計算
}
}
return false;
}
/// 取得指定時間的 UTC 偏移 (含夏令調整)
public TimeSpan GetUtcOffset(DateTime dateTime)
{
var offset = BaseUtcOffset;
if (IsDaylightSavingTime(dateTime))
offset += TimeSpan.FromHours(1); // 簡化:假設夏令 +1hr
return offset;
}
/// 將本地時間轉換為 UTC
public DateTime ConvertToUtc(DateTime dateTime) => dateTime - GetUtcOffset(dateTime);
/// 將 UTC 時間轉換為本地時間
public DateTime ConvertFromUtc(DateTime dateTime) => dateTime + GetUtcOffset(dateTime);
/// 取得指定年份的夏令時間變更資訊
public DaylightTime GetDaylightChanges(int year)
{
// 實際需依規則計算該年具體開始/結束日期
return new DaylightTime(
new DateTime(year, 3, 1), // 範例
new DateTime(year, 11, 1),
TimeSpan.FromHours(1));
}
// 靜態排序輔助
public static void Sort(List list) =>
list.Sort((a, b) => a.BaseUtcOffset.CompareTo(b.BaseUtcOffset));
}
public partial class Form1 : Form
{
private TimeZoneInfo[] _zones;
private void Form1_Load(object sender, EventArgs e)
{
_zones = TimeZoneInfo.GetSystemTimeZones();
cmbTimeZone.DataSource = _zones;
cmbTimeZone.DisplayMember = "DisplayName";
cmbTimeZone.ValueMember = "Id";
}
private void btnQuery_Click(object sender, EventArgs e)
{
if (cmbTimeZone.SelectedItem is TimeZoneInfo tzi)
{
DateTime input = DateTime.Parse(txtDate.Text);
lblInfo.Text = $"時區 ID: {tzi.Id}\n" +
$"顯示名稱: {tzi.DisplayName}\n" +
$"標準名稱: {tzi.StandardName}\n" +
$"夏令名稱: {tzi.DaylightName}\n" +
$"基準偏移: {tzi.BaseUtcOffset}\n" +
$"當前偏移: {tzi.GetUtcOffset(input)}\n" +
$"是否夏令: {tzi.IsDaylightSavingTime(input)}";
}
}
}
| Offset | Size | 欄位 | 說明 |
|---|---|---|---|
| 0 | 4 | Bias | 基準偏移 (分鐘),UTC = Local + Bias (如台北 -480) |
| 4 | 4 | StandardBias | 標準時間額外偏移 (通常 0) |
| 8 | 4 | DaylightBias | 夏令時間額外偏移 (通常 -60 = 夏令比標準早 1hr) |
| 12 | 16 | StandardDate | SYSTEMTIME: 標準時間生效規則 (月、第幾週、星期幾、時) |
| 28 | 16 | DaylightDate | SYSTEMTIME: 夏令時間生效規則 |
wMonth:1-12 (0 表示無規則)wDay:1-5 (第幾週),5 表示最後一週wDayOfWeek:0=週日 ... 6=週六wHour:0-23 (UTC 時間)3月 第2週 週日 02:00 → 夏令開始某些時區 (如美國、俄羅斯) 歷年調整夏令規則。登錄機碼 Dynamic DST 子機碼下以年份為值名儲存對應年份的 TZI。建構 AdjustmentRule 時需逐年建立,TimeZoneInfo 內部會依輸入日期自動選擇正確規則。
TimeZoneInfo (CoreFX) 或 IANA tz database (NodaTime)。GetSystemTimeZones() 結果,避免重複讀取登錄。System.TimeZoneInfo,功能更完整且經過測試。