MyImage.vb 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. Public Class MyImage
  2. Inherits Image
  3. #Region "公开属性"
  4. ''' <summary>
  5. ''' 网络图片的缓存有效期。
  6. ''' 在这个时间后,才会重新尝试下载图片。
  7. ''' </summary>
  8. Public FileCacheExpiredTime As New TimeSpan(7, 0, 0, 0) '7 天
  9. ''' <summary>
  10. ''' 是否允许将网络图片存储到本地用作缓存。
  11. ''' </summary>
  12. Public Property EnableCache As Boolean
  13. Get
  14. Return GetValue(EnableCacheProperty)
  15. End Get
  16. Set(value As Boolean)
  17. SetValue(EnableCacheProperty, value)
  18. End Set
  19. End Property
  20. Public Shared Shadows ReadOnly EnableCacheProperty As DependencyProperty = DependencyProperty.Register(
  21. "EnableCache", GetType(Boolean), GetType(MyImage), New PropertyMetadata(True))
  22. ''' <summary>
  23. ''' 与 Image 的 Source 类似。
  24. ''' 若输入以 http 开头的字符串,则会尝试下载图片然后显示,图片会保存为本地缓存。
  25. ''' 支持 WebP 格式的图片。
  26. ''' </summary>
  27. Public Shadows Property Source As String '覆写 Image 的 Source 属性
  28. Get
  29. Return _Source
  30. End Get
  31. Set(value As String)
  32. If value = "" Then value = Nothing
  33. If _Source = value Then Return
  34. _Source = value
  35. If Not IsInitialized Then Return '属性读取顺序修正:在完成 XAML 属性读取后再触发图片加载(#4868)
  36. Load()
  37. End Set
  38. End Property
  39. Private _Source As String = ""
  40. Public Shared Shadows ReadOnly SourceProperty As DependencyProperty = DependencyProperty.Register(
  41. "Source", GetType(String), GetType(MyImage), New PropertyMetadata(New PropertyChangedCallback(
  42. Sub(sender, e) If sender IsNot Nothing Then CType(sender, MyImage).Source = e.NewValue.ToString())))
  43. ''' <summary>
  44. ''' 当 Source 首次下载失败时,会从该备用地址加载图片。
  45. ''' </summary>
  46. Public Property FallbackSource As String
  47. Get
  48. Return _FallbackSource
  49. End Get
  50. Set(value As String)
  51. _FallbackSource = value
  52. End Set
  53. End Property
  54. Private _FallbackSource As String = Nothing
  55. ''' <summary>
  56. ''' 正在下载网络图片时显示的本地图片。
  57. ''' </summary>
  58. Public Property LoadingSource As String
  59. Get
  60. Return _LoadingSource
  61. End Get
  62. Set(value As String)
  63. _LoadingSource = value
  64. End Set
  65. End Property
  66. Private _LoadingSource As String = "pack://application:,,,/images/Icons/NoIcon.png"
  67. #End Region
  68. ''' <summary>
  69. ''' 实际被呈现的图片地址。
  70. ''' </summary>
  71. Public Property ActualSource As String
  72. Get
  73. Return _ActualSource
  74. End Get
  75. Set(value As String)
  76. If value = "" Then value = Nothing
  77. If _ActualSource = value Then Return
  78. _ActualSource = value
  79. Try
  80. Dim Bitmap As MyBitmap = If(value Is Nothing, Nothing, New MyBitmap(value)) '在这里先触发可能的文件读取,尽量避免在 UI 线程中读取文件
  81. RunInUiWait(Sub() MyBase.Source = Bitmap)
  82. Catch ex As Exception
  83. Log(ex, $"加载图片失败({value})")
  84. Try
  85. If value.StartsWithF(PathTemp) AndAlso File.Exists(value) Then File.Delete(value)
  86. Catch
  87. End Try
  88. End Try
  89. End Set
  90. End Property
  91. Private _ActualSource As String = Nothing
  92. Private Sub Load() _
  93. Handles Me.Initialized '属性读取顺序修正:在完成 XAML 属性读取后再触发图片加载(#4868)
  94. '空
  95. If Source Is Nothing Then
  96. ActualSource = Nothing
  97. Return
  98. End If
  99. '本地图片
  100. If Not Source.StartsWithF("http") Then
  101. ActualSource = Source
  102. Return
  103. End If
  104. '从缓存加载网络图片
  105. Dim Url As String = Source
  106. Dim Retried As Boolean = False
  107. Dim TempPath As String = GetTempPath(Url)
  108. Dim TempFile As New FileInfo(TempPath)
  109. Dim EnableCache As Boolean = Me.EnableCache
  110. If EnableCache AndAlso TempFile.Exists Then
  111. ActualSource = TempPath
  112. If (Date.Now - TempFile.LastWriteTime) < FileCacheExpiredTime Then Return '无需刷新缓存
  113. End If
  114. RunInNewThread(
  115. Sub()
  116. Dim TempDownloadingPath As String = Nothing
  117. Try
  118. RetryStart:
  119. '下载
  120. ActualSource = LoadingSource '显示加载中图片
  121. TempDownloadingPath = TempPath & RandomInteger(0, 10000000)
  122. Directory.CreateDirectory(GetPathFromFullPath(TempPath)) '重新实现下载,以避免携带 Header(#5072)
  123. Using Client As New Net.WebClient
  124. Client.DownloadFile(Url, TempDownloadingPath)
  125. End Using
  126. If Url <> Source AndAlso Url <> FallbackSource Then
  127. '已经更换了地址
  128. File.Delete(TempDownloadingPath)
  129. ElseIf EnableCache Then
  130. '保存缓存并显示
  131. If File.Exists(TempPath) Then File.Delete(TempPath)
  132. FileSystem.Rename(TempDownloadingPath, TempPath)
  133. RunInUi(Sub() ActualSource = TempPath)
  134. Else
  135. '直接显示
  136. RunInUiWait(Sub() ActualSource = TempDownloadingPath)
  137. File.Delete(TempDownloadingPath)
  138. End If
  139. Catch ex As Exception
  140. Try
  141. If TempPath IsNot Nothing Then File.Delete(TempPath)
  142. If TempDownloadingPath IsNot Nothing Then File.Delete(TempDownloadingPath)
  143. Catch
  144. End Try
  145. If Not Retried Then
  146. '更换备用地址
  147. Log(ex, $"下载图片可重试地失败({Url})", LogLevel.Developer)
  148. Retried = True
  149. Url = If(FallbackSource, Source)
  150. '空
  151. If Url Is Nothing Then
  152. ActualSource = Nothing
  153. Return
  154. End If
  155. '本地图片
  156. If Not Url.StartsWithF("http") Then
  157. ActualSource = Url
  158. Return
  159. End If
  160. '从缓存加载网络图片
  161. TempPath = GetTempPath(Url)
  162. TempFile = New FileInfo(TempPath)
  163. If EnableCache AndAlso TempFile.Exists() Then
  164. ActualSource = TempPath
  165. If (Date.Now - TempFile.CreationTime) < FileCacheExpiredTime Then Return '无需刷新缓存
  166. End If
  167. '下载
  168. If Source = Url Then Thread.Sleep(1000) '延迟 1s 重试
  169. GoTo RetryStart
  170. Else
  171. Log(ex, $"下载图片失败({Url})", LogLevel.Hint)
  172. End If
  173. End Try
  174. End Sub, "MyImage PicLoader " & GetUuid() & "#", ThreadPriority.BelowNormal)
  175. End Sub
  176. Public Shared Function GetTempPath(Url As String) As String
  177. Return $"{PathTemp}MyImage\{GetHash(Url)}.png"
  178. End Function
  179. End Class