Files
lserp_cs_6.0/插件库/Lskj.Control/Model/LabPicUrlPreview.cs
T
tdx 0eb41b53a7 SVN r964
SVN-Revision: r964
2025-09-05 03:01:57 +00:00

329 lines
12 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ============================================================================
// 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 + SemaphoreC# 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