0eb41b53a7
SVN-Revision: r964
329 lines
12 KiB
C#
329 lines
12 KiB
C#
// ============================================================================
|
||
// LabPicUrlPreview.cs (C# 7.3 / DevExpress 15.2 兼容)
|
||
// - 公用方法封装在一个类里
|
||
// - 事件绑定在一个地方 Attach(view)
|
||
// - 线程安全缓存 + 并发限流 + LRU 清理
|
||
// - 只在同步阶段写 e.Value;异步完成后刷新单元格
|
||
// ============================================================================
|
||
using System;
|
||
using System.Collections.Generic;
|
||
using System.Data;
|
||
using System.Drawing;
|
||
using System.IO;
|
||
using System.Linq;
|
||
using System.Net;
|
||
using System.Threading;
|
||
using System.Threading.Tasks;
|
||
using System.Windows.Forms;
|
||
|
||
using DevExpress.Data;
|
||
using DevExpress.XtraEditors.Controls;
|
||
using DevExpress.XtraEditors.Repository;
|
||
using DevExpress.XtraGrid;
|
||
using DevExpress.XtraGrid.Columns;
|
||
using DevExpress.XtraGrid.Views.Base;
|
||
using DevExpress.XtraGrid.Views.Grid;
|
||
using DevExpress.XtraGrid.Views.BandedGrid;
|
||
using Lskj.Business;
|
||
using Lskj.Control.Model;
|
||
// 若命名空间不同,请改成正确的 using
|
||
|
||
public static class LabPicUrlPreview
|
||
{
|
||
// ====== 对外:初始化图片列(创建 RepositoryItemPictureEdit、设为 Unbound) ======
|
||
public static void InitPicColumn(GridControl gridControl, GridColumn gridColumn, GridColumnModel model)
|
||
{
|
||
if (gridControl == null || gridColumn == null || model == null) return;
|
||
|
||
gridColumn.AppearanceCell.Options.UseTextOptions = true;
|
||
gridColumn.AppearanceCell.TextOptions.HAlignment = DevExpress.Utils.HorzAlignment.Center;
|
||
gridColumn.OptionsColumn.AllowEdit = false;
|
||
|
||
// 唯一列名(保持你原逻辑)
|
||
gridColumn.FieldName = string.Format("{0}{1}", model.FieldName, Guid.NewGuid());
|
||
gridColumn.Tag = model; // 事件里取模型
|
||
|
||
RepositoryItemPictureEdit pictureEdit = new RepositoryItemPictureEdit();
|
||
pictureEdit.ShowMenu = false;
|
||
pictureEdit.NullText = " ";
|
||
pictureEdit.SizeMode = PictureSizeMode.Zoom;
|
||
|
||
gridControl.RepositoryItems.Add(pictureEdit);
|
||
gridColumn.UnboundType = UnboundColumnType.Object;
|
||
gridColumn.ColumnEdit = pictureEdit;
|
||
}
|
||
|
||
// ====== 对外:统一事件绑定入口(只需调用一次) ======
|
||
public static void Attach(ColumnView view)
|
||
{
|
||
if (view == null) return;
|
||
view.CustomUnboundColumnData -= OnCustomUnboundColumnData;
|
||
view.CustomUnboundColumnData += OnCustomUnboundColumnData;
|
||
}
|
||
|
||
// ====== 事件实现(只在同步阶段写 e.Value;异步完成后刷新该单元格) ======
|
||
private static void OnCustomUnboundColumnData(object sender, CustomColumnDataEventArgs e)
|
||
{
|
||
ColumnView view = sender as ColumnView;
|
||
if (view == null) return;
|
||
|
||
GridColumnModel m = e.Column.Tag as GridColumnModel;
|
||
if (m == null || m.FieldType != ControlType.LabPicUrl || !e.IsGetData) return;
|
||
|
||
// 1) 用 ListSourceRowIndex → RowHandle(兼容 DevExpress 15.2)
|
||
int listIndex = e.ListSourceRowIndex;
|
||
int rowHandle = view.GetRowHandle(listIndex);
|
||
|
||
// 2) 读取原始 URL(优先 ListSource;兜底 e.Row)
|
||
string raw = null;
|
||
try
|
||
{
|
||
object val = view.GetListSourceRowCellValue(listIndex, m.FieldName);
|
||
raw = val == null ? null : Convert.ToString(val);
|
||
}
|
||
catch
|
||
{
|
||
DataRowView drv = e.Row as DataRowView;
|
||
if (drv != null && drv.Row != null && drv.Row.Table != null && drv.Row.Table.Columns.Contains(m.FieldName))
|
||
raw = Convert.ToString(drv.Row[m.FieldName]);
|
||
}
|
||
if (string.IsNullOrWhiteSpace(raw)) return;
|
||
|
||
// 3) 拼接完整 URL(相对路径 -> OA 根地址)
|
||
string url = ResolveFullUrl(raw);
|
||
|
||
// 4) 目标高(属性或方法都兼容;<=18 则用 30)
|
||
int targetHeight = NormalizeHeight(m);
|
||
|
||
// 5) 缓存命中:当次赋值(只在同步阶段写 e.Value)
|
||
Image img;
|
||
if (UrlImageCacheCompat.TryGet(url, targetHeight, out img))
|
||
{
|
||
e.Value = img;
|
||
return;
|
||
}
|
||
|
||
// 6) 未命中:安排异步下载;完成后刷新该单元格(不要在异步线程里改 e.Value)
|
||
System.Windows.Forms.Control invoker = view.GridControl; // UI 回调对象
|
||
UrlImageCacheCompat.GetOrCreateAsync(url, targetHeight, invoker, delegate ()
|
||
{
|
||
RefreshCellCompat(view, rowHandle, e.Column);
|
||
});
|
||
}
|
||
|
||
// ====== 视图安全刷新:GridView/BandedGridView 定向刷新,其他降级为 RefreshData ======
|
||
private static void RefreshCellCompat(ColumnView view, int rowHandle, GridColumn column)
|
||
{
|
||
try
|
||
{
|
||
GridView gv = view as GridView;
|
||
if (gv != null)
|
||
{
|
||
if (rowHandle >= 0) gv.RefreshRowCell(rowHandle, column);
|
||
else gv.RefreshData();
|
||
return;
|
||
}
|
||
|
||
BandedGridView bgv = view as BandedGridView;
|
||
if (bgv != null)
|
||
{
|
||
if (rowHandle >= 0) bgv.RefreshRowCell(rowHandle, column);
|
||
else bgv.RefreshData();
|
||
return;
|
||
}
|
||
|
||
view.RefreshData();
|
||
}
|
||
catch
|
||
{
|
||
try { view.RefreshData(); } catch { }
|
||
}
|
||
}
|
||
|
||
// ====== 兼容“属性或无参方法”的高度读取(避免 m.LabPicUrlHeight 被当作方法组出错) ======
|
||
private static int NormalizeHeight(object model)
|
||
{
|
||
try
|
||
{
|
||
var t = model.GetType();
|
||
|
||
// 属性
|
||
var pi = t.GetProperty("LabPicUrlHeight");
|
||
if (pi != null)
|
||
{
|
||
int v = Convert.ToInt32(pi.GetValue(model, null));
|
||
return v <= 18 ? 30 : v;
|
||
}
|
||
|
||
// 无参方法
|
||
var mi = t.GetMethod("LabPicUrlHeight", Type.EmptyTypes);
|
||
if (mi != null)
|
||
{
|
||
int v = Convert.ToInt32(mi.Invoke(model, null));
|
||
return v <= 18 ? 30 : v;
|
||
}
|
||
}
|
||
catch { }
|
||
return 30;
|
||
}
|
||
|
||
// ====== 解析完整 URL:相对路径 -> SystemInfo.Instance.OAUrl + 相对 ======
|
||
private static string ResolveFullUrl(string raw)
|
||
{
|
||
if (string.IsNullOrEmpty(raw)) return raw;
|
||
if (raw.StartsWith("http", StringComparison.OrdinalIgnoreCase)) return raw;
|
||
|
||
try
|
||
{
|
||
// 直接使用你项目里的 SystemInfo(若没有可替换为你自己的配置)
|
||
string oa = SystemInfo.Instance.OAUrl;
|
||
if (string.IsNullOrEmpty(oa)) return raw.TrimStart('/');
|
||
if (!oa.EndsWith("/")) oa += "/";
|
||
return oa + raw.TrimStart('/');
|
||
}
|
||
catch
|
||
{
|
||
return raw.TrimStart('/');
|
||
}
|
||
}
|
||
|
||
// ====== 线程安全缓存(WebClient + Dictionary + Semaphore,C# 7.3 兼容) ======
|
||
private static class UrlImageCacheCompat
|
||
{
|
||
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);
|
||
|
||
// 旧框架用传统 Semaphore,限制并发下载数量
|
||
private static readonly Semaphore _throttle = new Semaphore(4, 4); // 同时最多 4 个下载
|
||
private const int Capacity = 300; // 最多缓存 300 张缩略图
|
||
|
||
static UrlImageCacheCompat()
|
||
{
|
||
// 尽量启用 TLS1.2(老系统需要)
|
||
try
|
||
{
|
||
const System.Security.Authentication.SslProtocols _Tls12 =
|
||
(System.Security.Authentication.SslProtocols)0x00000C00;
|
||
ServicePointManager.SecurityProtocol |= (SecurityProtocolType)_Tls12;
|
||
}
|
||
catch { }
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
// 同 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)
|
||
{
|
||
int h = targetHeight <= 0 ? 30 : targetHeight;
|
||
if (h < 18) h = 18;
|
||
|
||
double ratio = (double)h / (double)bmp.Height;
|
||
int w = (int)Math.Round((double)bmp.Width * ratio);
|
||
if (w < 1) w = 1;
|
||
|
||
// 只缓存缩略图,避免内存暴涨
|
||
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 |