SVN r965
SVN-Revision: r965
This commit is contained in:
@@ -1,9 +1,8 @@
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
// LabPicUrlPreview.cs (C# 7.3 / DevExpress 15.2 兼容)
|
// LabPicUrlPreview.cs (C# 7.3 / DevExpress 15.2 兼容)
|
||||||
// - 公用方法封装在一个类里
|
// - 公用方法封装在一个类里:InitPicColumn / Attach / AttachPreview
|
||||||
// - 事件绑定在一个地方 Attach(view)
|
// - 线程安全缓存:缩略图 & 原图 分离缓存;并发限流 + LRU 清理
|
||||||
// - 线程安全缓存 + 并发限流 + LRU 清理
|
// - 只在同步阶段写 e.Value;异步完成后刷新单元格或更新预览窗体
|
||||||
// - 只在同步阶段写 e.Value;异步完成后刷新单元格
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
@@ -24,9 +23,11 @@ using DevExpress.XtraGrid.Columns;
|
|||||||
using DevExpress.XtraGrid.Views.Base;
|
using DevExpress.XtraGrid.Views.Base;
|
||||||
using DevExpress.XtraGrid.Views.Grid;
|
using DevExpress.XtraGrid.Views.Grid;
|
||||||
using DevExpress.XtraGrid.Views.BandedGrid;
|
using DevExpress.XtraGrid.Views.BandedGrid;
|
||||||
using Lskj.Business;
|
using DevExpress.XtraGrid.Views.Grid.ViewInfo;
|
||||||
|
using DevExpress.XtraGrid.Views.BandedGrid.ViewInfo;
|
||||||
using Lskj.Control.Model;
|
using Lskj.Control.Model;
|
||||||
// 若命名空间不同,请改成正确的 using
|
using Lskj.Control;
|
||||||
|
using Lskj.Business;
|
||||||
|
|
||||||
public static class LabPicUrlPreview
|
public static class LabPicUrlPreview
|
||||||
{
|
{
|
||||||
@@ -53,7 +54,7 @@ public static class LabPicUrlPreview
|
|||||||
gridColumn.ColumnEdit = pictureEdit;
|
gridColumn.ColumnEdit = pictureEdit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ====== 对外:统一事件绑定入口(只需调用一次) ======
|
// ====== 对外:绑定“取缩略图”的事件(只需调用一次) ======
|
||||||
public static void Attach(ColumnView view)
|
public static void Attach(ColumnView view)
|
||||||
{
|
{
|
||||||
if (view == null) return;
|
if (view == null) return;
|
||||||
@@ -61,7 +62,15 @@ public static class LabPicUrlPreview
|
|||||||
view.CustomUnboundColumnData += OnCustomUnboundColumnData;
|
view.CustomUnboundColumnData += OnCustomUnboundColumnData;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ====== 事件实现(只在同步阶段写 e.Value;异步完成后刷新该单元格) ======
|
// ====== 对外:绑定“双击大图预览”的事件(只需调用一次) ======
|
||||||
|
public static void AttachPreview(ColumnView view)
|
||||||
|
{
|
||||||
|
if (view == null) return;
|
||||||
|
view.DoubleClick -= OnViewDoubleClick;
|
||||||
|
view.DoubleClick += OnViewDoubleClick;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== 缩略图事件实现 =====================
|
||||||
private static void OnCustomUnboundColumnData(object sender, CustomColumnDataEventArgs e)
|
private static void OnCustomUnboundColumnData(object sender, CustomColumnDataEventArgs e)
|
||||||
{
|
{
|
||||||
ColumnView view = sender as ColumnView;
|
ColumnView view = sender as ColumnView;
|
||||||
@@ -97,7 +106,7 @@ public static class LabPicUrlPreview
|
|||||||
|
|
||||||
// 5) 缓存命中:当次赋值(只在同步阶段写 e.Value)
|
// 5) 缓存命中:当次赋值(只在同步阶段写 e.Value)
|
||||||
Image img;
|
Image img;
|
||||||
if (UrlImageCacheCompat.TryGet(url, targetHeight, out img))
|
if (ImageCache.TryGetThumb(url, targetHeight, out img))
|
||||||
{
|
{
|
||||||
e.Value = img;
|
e.Value = img;
|
||||||
return;
|
return;
|
||||||
@@ -105,12 +114,105 @@ public static class LabPicUrlPreview
|
|||||||
|
|
||||||
// 6) 未命中:安排异步下载;完成后刷新该单元格(不要在异步线程里改 e.Value)
|
// 6) 未命中:安排异步下载;完成后刷新该单元格(不要在异步线程里改 e.Value)
|
||||||
System.Windows.Forms.Control invoker = view.GridControl; // UI 回调对象
|
System.Windows.Forms.Control invoker = view.GridControl; // UI 回调对象
|
||||||
UrlImageCacheCompat.GetOrCreateAsync(url, targetHeight, invoker, delegate ()
|
ImageCache.GetThumbAsync(url, targetHeight, invoker, delegate ()
|
||||||
{
|
{
|
||||||
RefreshCellCompat(view, rowHandle, e.Column);
|
RefreshCellCompat(view, rowHandle, e.Column);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===================== 双击预览(原图) =====================
|
||||||
|
private static void OnViewDoubleClick(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
ColumnView view = sender as ColumnView;
|
||||||
|
if (view == null || view.GridControl == null) return;
|
||||||
|
|
||||||
|
// 命中测试(GridView / BandedGridView 兼容)
|
||||||
|
GridColumn hitColumn = null;
|
||||||
|
int rowHandle = -1;
|
||||||
|
|
||||||
|
Point pt = view.GridControl.PointToClient(Control.MousePosition);
|
||||||
|
|
||||||
|
GridView gv = view as GridView;
|
||||||
|
if (gv != null)
|
||||||
|
{
|
||||||
|
GridHitInfo hi = gv.CalcHitInfo(pt);
|
||||||
|
if (hi == null || !hi.InRowCell || hi.Column == null) return;
|
||||||
|
hitColumn = hi.Column;
|
||||||
|
rowHandle = hi.RowHandle;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
BandedGridView bgv = view as BandedGridView;
|
||||||
|
if (bgv == null) return;
|
||||||
|
BandedGridHitInfo hi2 = bgv.CalcHitInfo(pt);
|
||||||
|
if (hi2 == null || !hi2.InRowCell || hi2.Column == null) return;
|
||||||
|
hitColumn = hi2.Column;
|
||||||
|
rowHandle = hi2.RowHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只处理 LabPicUrl 列
|
||||||
|
GridColumnModel m = hitColumn.Tag as GridColumnModel;
|
||||||
|
if (m == null || m.FieldType != ControlType.LabPicUrl) return;
|
||||||
|
|
||||||
|
// 取当前行的 URL(优先 RowHandle 取值)
|
||||||
|
string raw = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
object val = view.GetRowCellValue(rowHandle, m.FieldName);
|
||||||
|
raw = val == null ? null : Convert.ToString(val);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// 兜底:通过 ListSourceRowIndex
|
||||||
|
int listIndex = view.GetDataSourceRowIndex(rowHandle);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
object val2 = view.GetListSourceRowCellValue(listIndex, m.FieldName);
|
||||||
|
raw = val2 == null ? null : Convert.ToString(val2);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
if (string.IsNullOrWhiteSpace(raw)) return;
|
||||||
|
|
||||||
|
string url = ResolveFullUrl(raw);
|
||||||
|
|
||||||
|
// —— 打开你的预览窗体(先以缩略图占位) ——
|
||||||
|
Image thumb = null;
|
||||||
|
ImageCache.TryGetThumb(url, NormalizeHeight(m), out thumb);
|
||||||
|
|
||||||
|
FrmPicture frm = new FrmPicture();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 这些属性基于你原有窗体接口
|
||||||
|
frm.PicUrlColumns = view.Columns
|
||||||
|
.Where(c => c.Tag is GridColumnModel cm && cm.FieldType == ControlType.LabPicUrl)
|
||||||
|
.ToList();
|
||||||
|
frm.FocusedRow = (view as GridView) != null ? (view as GridView).GetDataRow(rowHandle) : null;
|
||||||
|
frm.FocusedColumn = hitColumn;
|
||||||
|
frm.Text = "预览";
|
||||||
|
frm.Picture = thumb; // 先用缩略图(可能为 null)
|
||||||
|
frm.WindowState = FormWindowState.Maximized;
|
||||||
|
frm.StartPosition = FormStartPosition.CenterScreen;
|
||||||
|
|
||||||
|
// 异步拉原图,完成后更新窗体
|
||||||
|
ImageCache.GetFullAsync(url, frm, delegate (Image full)
|
||||||
|
{
|
||||||
|
if (full == null) return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!frm.IsDisposed) frm.Picture = full;
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
});
|
||||||
|
|
||||||
|
frm.ShowDialog();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (frm != null && !frm.IsDisposed) frm.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ====== 视图安全刷新:GridView/BandedGridView 定向刷新,其他降级为 RefreshData ======
|
// ====== 视图安全刷新:GridView/BandedGridView 定向刷新,其他降级为 RefreshData ======
|
||||||
private static void RefreshCellCompat(ColumnView view, int rowHandle, GridColumn column)
|
private static void RefreshCellCompat(ColumnView view, int rowHandle, GridColumn column)
|
||||||
{
|
{
|
||||||
@@ -175,7 +277,6 @@ public static class LabPicUrlPreview
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// 直接使用你项目里的 SystemInfo(若没有可替换为你自己的配置)
|
|
||||||
string oa = SystemInfo.Instance.OAUrl;
|
string oa = SystemInfo.Instance.OAUrl;
|
||||||
if (string.IsNullOrEmpty(oa)) return raw.TrimStart('/');
|
if (string.IsNullOrEmpty(oa)) return raw.TrimStart('/');
|
||||||
if (!oa.EndsWith("/")) oa += "/";
|
if (!oa.EndsWith("/")) oa += "/";
|
||||||
@@ -187,28 +288,268 @@ public static class LabPicUrlPreview
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ====== 线程安全缓存(WebClient + Dictionary + Semaphore,C# 7.3 兼容) ======
|
// ====== 图片缓存(缩略图 & 原图)—— WebClient + Dictionary + Semaphore,C# 7.3 兼容 ======
|
||||||
private static class UrlImageCacheCompat
|
private static class ImageCache
|
||||||
{
|
{
|
||||||
private class CacheEntry
|
// —— 公共接口(缩略图) ——
|
||||||
|
public static bool TryGetThumb(string url, int targetHeight, out Image img)
|
||||||
{
|
{
|
||||||
public Image Image;
|
return ThumbCache.TryGet(url, targetHeight, out img);
|
||||||
public DateTime LastHitUtc;
|
}
|
||||||
|
public static void GetThumbAsync(string url, int targetHeight, System.Windows.Forms.Control invoker, Action onReady)
|
||||||
|
{
|
||||||
|
ThumbCache.GetOrCreateAsync(url, targetHeight, invoker, onReady);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static readonly object _lock = new object();
|
// —— 公共接口(原图) ——
|
||||||
private static readonly Dictionary<string, CacheEntry> _cache =
|
public static bool TryGetFull(string url, out Image img)
|
||||||
new Dictionary<string, CacheEntry>(StringComparer.OrdinalIgnoreCase);
|
{
|
||||||
private static readonly Dictionary<string, Task<Image>> _tasks =
|
return FullCache.TryGet(url, out img);
|
||||||
new Dictionary<string, Task<Image>>(StringComparer.OrdinalIgnoreCase);
|
}
|
||||||
|
public static void GetFullAsync(string url, System.Windows.Forms.Control invoker, Action<Image> onReady)
|
||||||
|
{
|
||||||
|
FullCache.GetOrCreateAsync(url, invoker, onReady);
|
||||||
|
}
|
||||||
|
|
||||||
// 旧框架用传统 Semaphore,限制并发下载数量
|
// ===== 缩略图缓存实现 =====
|
||||||
private static readonly Semaphore _throttle = new Semaphore(4, 4); // 同时最多 4 个下载
|
private static class ThumbCache
|
||||||
private const int Capacity = 300; // 最多缓存 300 张缩略图
|
{
|
||||||
|
private class CacheEntry
|
||||||
|
{
|
||||||
|
public Image Image;
|
||||||
|
public DateTime LastHitUtc;
|
||||||
|
}
|
||||||
|
|
||||||
static UrlImageCacheCompat()
|
private static readonly object _lock = new object();
|
||||||
|
private static readonly Dictionary<string, CacheEntry> _cache =
|
||||||
|
new Dictionary<string, CacheEntry>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
private static readonly Dictionary<string, Task<Image>> _tasks =
|
||||||
|
new Dictionary<string, Task<Image>>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
private static readonly Semaphore _throttle = new Semaphore(4, 4); // 并发 4
|
||||||
|
private const int Capacity = 300;
|
||||||
|
|
||||||
|
static ThumbCache()
|
||||||
|
{
|
||||||
|
EnableTls12();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string MakeKey(string url, int h)
|
||||||
|
{
|
||||||
|
return url + "#h=" + h.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TryGet(string url, int targetHeight, out Image image)
|
||||||
|
{
|
||||||
|
string key = MakeKey(url, targetHeight);
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
CacheEntry e;
|
||||||
|
if (_cache.TryGetValue(key, out e) && e != null && e.Image != null)
|
||||||
|
{
|
||||||
|
e.LastHitUtc = DateTime.UtcNow;
|
||||||
|
_cache[key] = e;
|
||||||
|
image = e.Image;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
image = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void GetOrCreateAsync(string url, int targetHeight, System.Windows.Forms.Control uiInvoker, Action onReady)
|
||||||
|
{
|
||||||
|
string key = MakeKey(url, targetHeight);
|
||||||
|
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
if (_cache.ContainsKey(key))
|
||||||
|
{
|
||||||
|
BeginInvokeSafe(uiInvoker, onReady);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Task<Image> exist;
|
||||||
|
if (_tasks.TryGetValue(key, out exist))
|
||||||
|
{
|
||||||
|
exist.ContinueWith(delegate { BeginInvokeSafe(uiInvoker, onReady); }, TaskScheduler.Default);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Task<Image> task = Task.Factory.StartNew<Image>(delegate
|
||||||
|
{
|
||||||
|
_throttle.WaitOne();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
byte[] bytes = DownloadBytes(url);
|
||||||
|
if (bytes == null) return null;
|
||||||
|
using (Bitmap src = BytesToBitmap(bytes))
|
||||||
|
{
|
||||||
|
return ScaleBitmap(src, targetHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { return null; }
|
||||||
|
finally { try { _throttle.Release(); } catch { } }
|
||||||
|
}, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default).ContinueWith<Image>(delegate (Task<Image> t)
|
||||||
|
{
|
||||||
|
Image result = t.Result;
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
_tasks.Remove(key);
|
||||||
|
if (result != null)
|
||||||
|
{
|
||||||
|
_cache[key] = new CacheEntry { Image = result, LastHitUtc = DateTime.UtcNow };
|
||||||
|
TrimIfNeededUnsafe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}, TaskScheduler.Default);
|
||||||
|
|
||||||
|
_tasks[key] = task;
|
||||||
|
task.ContinueWith(delegate { BeginInvokeSafe(uiInvoker, onReady); }, TaskScheduler.Default);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TrimIfNeededUnsafe()
|
||||||
|
{
|
||||||
|
if (_cache.Count <= Capacity) return;
|
||||||
|
List<KeyValuePair<string, CacheEntry>> list = _cache.ToList();
|
||||||
|
list.Sort(delegate (KeyValuePair<string, CacheEntry> a, KeyValuePair<string, CacheEntry> b)
|
||||||
|
{
|
||||||
|
return a.Value.LastHitUtc.CompareTo(b.Value.LastHitUtc);
|
||||||
|
});
|
||||||
|
int removeCount = Math.Max(1, Capacity / 10);
|
||||||
|
for (int i = 0; i < removeCount && i < list.Count; i++)
|
||||||
|
{
|
||||||
|
string k = list[i].Key;
|
||||||
|
CacheEntry e;
|
||||||
|
if (_cache.TryGetValue(k, out e))
|
||||||
|
{
|
||||||
|
_cache.Remove(k);
|
||||||
|
try { if (e.Image != null) e.Image.Dispose(); } catch { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 原图缓存实现(容量更小) =====
|
||||||
|
private static class FullCache
|
||||||
|
{
|
||||||
|
private class CacheEntry
|
||||||
|
{
|
||||||
|
public Image Image;
|
||||||
|
public DateTime LastHitUtc;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly object _lock = new object();
|
||||||
|
private static readonly Dictionary<string, CacheEntry> _cache =
|
||||||
|
new Dictionary<string, CacheEntry>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
private static readonly Dictionary<string, Task<Image>> _tasks =
|
||||||
|
new Dictionary<string, Task<Image>>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
private static readonly Semaphore _throttle = new Semaphore(2, 2); // 原图并发更小
|
||||||
|
private const int Capacity = 50;
|
||||||
|
|
||||||
|
static FullCache()
|
||||||
|
{
|
||||||
|
EnableTls12();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TryGet(string url, out Image image)
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
CacheEntry e;
|
||||||
|
if (_cache.TryGetValue(url, out e) && e != null && e.Image != null)
|
||||||
|
{
|
||||||
|
e.LastHitUtc = DateTime.UtcNow;
|
||||||
|
_cache[url] = e;
|
||||||
|
image = e.Image;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
image = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void GetOrCreateAsync(string url, System.Windows.Forms.Control uiInvoker, Action<Image> onReady)
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
Image cached;
|
||||||
|
if (TryGet(url, out cached))
|
||||||
|
{
|
||||||
|
BeginInvokeSafe(uiInvoker, delegate { onReady(cached); });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Task<Image> exist;
|
||||||
|
if (_tasks.TryGetValue(url, out exist))
|
||||||
|
{
|
||||||
|
exist.ContinueWith(t => BeginInvokeSafe(uiInvoker, delegate { onReady(t.Result); }), TaskScheduler.Default);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Task<Image> task = Task.Factory.StartNew<Image>(delegate
|
||||||
|
{
|
||||||
|
_throttle.WaitOne();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
byte[] bytes = DownloadBytes(url);
|
||||||
|
if (bytes == null) return null;
|
||||||
|
using (Bitmap bmp = BytesToBitmap(bytes))
|
||||||
|
{
|
||||||
|
// 原图:不缩放,复制一份避免流依赖
|
||||||
|
return CopyBitmap(bmp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { return null; }
|
||||||
|
finally { try { _throttle.Release(); } catch { } }
|
||||||
|
}, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default).ContinueWith<Image>(delegate (Task<Image> t)
|
||||||
|
{
|
||||||
|
Image result = t.Result;
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
_tasks.Remove(url);
|
||||||
|
if (result != null)
|
||||||
|
{
|
||||||
|
_cache[url] = new CacheEntry { Image = result, LastHitUtc = DateTime.UtcNow };
|
||||||
|
TrimIfNeededUnsafe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}, TaskScheduler.Default);
|
||||||
|
|
||||||
|
_tasks[url] = task;
|
||||||
|
task.ContinueWith(t => BeginInvokeSafe(uiInvoker, delegate { onReady(t.Result); }), TaskScheduler.Default);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TrimIfNeededUnsafe()
|
||||||
|
{
|
||||||
|
if (_cache.Count <= Capacity) return;
|
||||||
|
List<KeyValuePair<string, CacheEntry>> list = _cache.ToList();
|
||||||
|
list.Sort(delegate (KeyValuePair<string, CacheEntry> a, KeyValuePair<string, CacheEntry> b)
|
||||||
|
{
|
||||||
|
return a.Value.LastHitUtc.CompareTo(b.Value.LastHitUtc);
|
||||||
|
});
|
||||||
|
int removeCount = Math.Max(1, Capacity / 10);
|
||||||
|
for (int i = 0; i < removeCount && i < list.Count; i++)
|
||||||
|
{
|
||||||
|
string k = list[i].Key;
|
||||||
|
CacheEntry e;
|
||||||
|
if (_cache.TryGetValue(k, out e))
|
||||||
|
{
|
||||||
|
_cache.Remove(k);
|
||||||
|
try { if (e.Image != null) e.Image.Dispose(); } catch { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 共享底层工具 =====
|
||||||
|
private static void EnableTls12()
|
||||||
{
|
{
|
||||||
// 尽量启用 TLS1.2(老系统需要)
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
const System.Security.Authentication.SslProtocols _Tls12 =
|
const System.Security.Authentication.SslProtocols _Tls12 =
|
||||||
@@ -218,112 +559,19 @@ public static class LabPicUrlPreview
|
|||||||
catch { }
|
catch { }
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string MakeKey(string url, int h)
|
private static byte[] DownloadBytes(string url)
|
||||||
{
|
{
|
||||||
return url + "#h=" + h.ToString();
|
using (WebClient wc = new WebClient())
|
||||||
}
|
|
||||||
|
|
||||||
public static bool TryGet(string url, int targetHeight, out Image image)
|
|
||||||
{
|
|
||||||
string key = MakeKey(url, targetHeight);
|
|
||||||
lock (_lock)
|
|
||||||
{
|
{
|
||||||
CacheEntry e;
|
return wc.DownloadData(url);
|
||||||
if (_cache.TryGetValue(key, out e) && e != null && e.Image != null)
|
|
||||||
{
|
|
||||||
e.LastHitUtc = DateTime.UtcNow;
|
|
||||||
_cache[key] = e;
|
|
||||||
image = e.Image;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
image = null;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void GetOrCreateAsync(string url, int targetHeight, System.Windows.Forms.Control uiInvoker, Action onReady)
|
|
||||||
{
|
|
||||||
string key = MakeKey(url, targetHeight);
|
|
||||||
|
|
||||||
lock (_lock)
|
|
||||||
{
|
|
||||||
// 已缓存:直接回调
|
|
||||||
if (_cache.ContainsKey(key))
|
|
||||||
{
|
|
||||||
BeginInvokeSafe(uiInvoker, onReady);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 同 key 只下载一次:已有任务则挂回调
|
|
||||||
Task<Image> exist;
|
|
||||||
if (_tasks.TryGetValue(key, out exist))
|
|
||||||
{
|
|
||||||
exist.ContinueWith(delegate { BeginInvokeSafe(uiInvoker, onReady); }, TaskScheduler.Default);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 启动新任务(受限流)
|
|
||||||
Task<Image> task = Task.Factory.StartNew<Image>(delegate
|
|
||||||
{
|
|
||||||
_throttle.WaitOne();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using (WebClient wc = new WebClient())
|
|
||||||
{
|
|
||||||
byte[] bytes = wc.DownloadData(url);
|
|
||||||
using (MemoryStream ms = new MemoryStream(bytes))
|
|
||||||
using (Bitmap bmp = new Bitmap(ms))
|
|
||||||
{
|
|
||||||
return ScaleBitmap(bmp, targetHeight);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
try { _throttle.Release(); } catch { }
|
|
||||||
}
|
|
||||||
}, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default).ContinueWith<Image>(delegate (Task<Image> t)
|
|
||||||
{
|
|
||||||
Image result = t.Result;
|
|
||||||
lock (_lock)
|
|
||||||
{
|
|
||||||
_tasks.Remove(key);
|
|
||||||
if (result != null)
|
|
||||||
{
|
|
||||||
_cache[key] = new CacheEntry { Image = result, LastHitUtc = DateTime.UtcNow };
|
|
||||||
TrimIfNeededUnsafe();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}, TaskScheduler.Default);
|
|
||||||
|
|
||||||
_tasks[key] = task;
|
|
||||||
|
|
||||||
task.ContinueWith(delegate { BeginInvokeSafe(uiInvoker, onReady); }, TaskScheduler.Default);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Image ScaleBitmap(Bitmap bmp, int targetHeight)
|
private static Bitmap BytesToBitmap(byte[] bytes)
|
||||||
{
|
{
|
||||||
int h = targetHeight <= 0 ? 30 : targetHeight;
|
using (MemoryStream ms = new MemoryStream(bytes))
|
||||||
if (h < 18) h = 18;
|
{
|
||||||
|
// 为避免对流的依赖,先 Image.FromStream,再复制
|
||||||
double ratio = (double)h / (double)bmp.Height;
|
using (Image img = Image.FromStream(ms))
|
||||||
int w = (int)Math.Round((double)bmp.Width * ratio);
|
{
|
||||||
if (w < 1) w = 1;
|
return new B
|
||||||
|
|
||||||
// 只缓存缩略图,避免内存暴涨
|
|
||||||
return new Bitmap(bmp, new Size(w, h));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void TrimIfNeededUnsafe()
|
|
||||||
{
|
|
||||||
if (_cache.Count <= Capacity) return;
|
|
||||||
|
|
||||||
// 简单 LRU:移除最早命中的 10%
|
|
||||||
List<KeyValuePair<string, CacheEntry>> list = _cache.ToList();
|
|
||||||
list.Sort(delegate (KeyValuePair<string, CacheEntry> a, KeyValuePair<string, Ca
|
|
||||||
Reference in New Issue
Block a user