WPF仿QQ聊天框表情文字混排實現
二話不說。先上圖
圖中分別有檔案、文字+表情、純文字的展示,對於同一個list不同的展示形式,很明顯,應該用多個DataTemplate,那麼也就需要DataTemplateSelector了:
class MessageDataTemplateSelector : DataTemplateSelector
{
public override System.Windows.DataTemplate SelectTemplate(object item, System.Windows.DependencyObject container)
{
Window win = Application.Current.MainWindow;
var myUserNo = UserLoginInfo.GetInstance().UserNo;
if (item!=null)
{
NIMIMMessage m = item as NIMIMMessage;
if (m.SenderID==myUserNo)
{
switch (m.MessageType)
{
case NIMMessageType.kNIMMessageTypeAudio:
case NIMMessageType.kNIMMessageTypeVideo:
return win.FindResource("self_media") as DataTemplate;
case NIMMessageType.kNIMMessageTypeFile:
return win.FindResource("self_file") as DataTemplate;
case NIMMessageType.kNIMMessageTypeImage:
return win.FindResource("self_image") as DataTemplate;
case NIMMessageType.kNIMMessageTypeText:
return win.FindResource("self_text") as DataTemplate;
default:
break;
}
}
else
{
switch (m.MessageType)
{
case NIMMessageType.kNIMMessageTypeAudio:
case NIMMessageType.kNIMMessageTypeVideo:
return win.FindResource("friend_media") as DataTemplate;
case NIMMessageType.kNIMMessageTypeFile:
return win.FindResource("friend_file") as DataTemplate;
case NIMMessageType.kNIMMessageTypeImage:
return win.FindResource("friend_image") as DataTemplate;
case NIMMessageType.kNIMMessageTypeText:
return win.FindResource("friend_text") as DataTemplate;
default:
break;
}
}
}
return null;
}
}
以上一共有8個DateTemplate,friend和self的區別就在於一個在左一個在右,我這邊就放friend_text的樣式程式碼好了,因為本篇主要說的是表情和文字的混排:
<Window.Resources>
<DataTemplate x:Key="friend_text">
<Grid Margin="12 6">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="32"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Image Source="{Binding TalkID}" HorizontalAlignment="Center" VerticalAlignment="Center">
<Image.Clip>
<EllipseGeometry RadiusX="16" RadiusY="16" Center="16 16"/>
</Image.Clip>
</Image>
<Grid HorizontalAlignment="Left" Grid.Column="1" Background="Transparent" VerticalAlignment="Center" Margin="12 0 0 0">
<Border CornerRadius="8" Background="#F0F0F0" Padding="6" >
<xctk:RichTextBox FontSize="14"
VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Disabled"
Text="{Binding TextContent,Converter={StaticResource ShowImageOrTextConverter}}"
VerticalAlignment="Center"
BorderThickness="0"
IsReadOnly="True"
Background="Transparent">
<FlowDocument Name="rtbFlowDoc" PageWidth="{Binding MessageWidth}"/>
<xctk:RichTextBox.TextFormatter>
<xctk:XamlFormatter/>
</xctk:RichTextBox.TextFormatter>
</xctk:RichTextBox>
</Border>
</Grid>
</Grid>
</DataTemplate>
</Window.Resources>
以上可以看到,我們使用了RichTextBox這個控制元件,不過並不是原生的,而是xceed.wpf.toolkit下的,所以別忘了引入名稱空間:
xmlns:xctk=”http://schemas.xceed.com/wpf/xaml/toolkit”
為什麼要引用這個控制元件,因為它支援繫結Text -:)
上一篇已經提到過,我們的IM用的是網易雲的SDK,在這個SDK裡表情也是通過文字傳送的,如[微笑]就代表微笑的表情。
那麼問題就很明顯了——怎麼解析這段帶有表情的文字,並且把表情顯示出來。
private Text GenerateTextMessage(NIMTextMessage m, string senderId)
{
Text text = new Text();
text.TextContent = m.TextContent;
text.TalkID = friendHeadUrl;
if (!string.IsNullOrEmpty(senderId))
{
text.SenderID = senderId;
}
var txt = text.TextContent;
int length = 0;
if (txt.Contains("[") && txt.Contains("]"))
{
StringBuilder str = new StringBuilder();
List<EmoticonText> emoticonText = new List<EmoticonText>();
char[] chars = txt.ToCharArray();
for (int i = 0; i < chars.Length; i++)
{
char c = chars[i];
if (chars[i] == '[')
{
emoticonText.Add(new EmoticonText { Key = "text", Value = str.ToString() });
str.Clear();
int f = txt.IndexOf(']', i);
string es = txt.Substring(i, f - i + 1);
XElement node = elementCollection.Where(a => a.Attribute("Tag").Value == es).FirstOrDefault();
if (node == null)
{
str.Append(es);
length += (f - i + 1) * 14;
}
else
{
emoticonText.Add(new EmoticonText { Key = "emoticon", Value = "../Resources/Emoticon/" + node.Attribute("File").Value });
i = f;
length += 32;
}
}
else
{
str.Append(c);
length += 14;
}
}
text.TextContent = JsonConvert.SerializeObject(emoticonText);
}
else
{
List<EmoticonText> textStr = new List<EmoticonText>();
textStr.Add(new EmoticonText() { Key = "text", Value = txt });
text.TextContent = JsonConvert.SerializeObject(textStr);
length = txt.Length * 14;
}
length += 24;
if (length < 38 * 14)
{
text.MessageWidth = length.ToString();
}
return text;
}
Text是自定義的一個實體,它包含需要繫結到xaml的屬性。這裡我用EmoticonText這個實體來區分是表情圖片還是純文字。另外,有的同學可能有疑問,這裡的length是幹嘛用的,看前面那個DataTemplate,其中PageWidth=”{Binding MessageWidth}”,所以這個length是計算當前RichTextBox寬度的,為什麼要手動計算寬度呢?因為RichTextBox貌似沒提供根據內容自適應寬度,如果我是用TextBox的話,其寬度就會根據其中顯示內容的長短進行自適應;那為什麼要乘14加28什麼的呢?因為我這個是按字元個數來算寬度的,當以14為係數因子的時候,中文顯示勉強滿意,但是如果是純英文或數字就不行了,這也是為什麼截圖裡RichTextBox右邊還空那麼一塊;最後加24是因為邊距,38是一行最多顯示38箇中文,如果超過了38箇中文還對其計算寬度的話,就會導致其不換行了。
如果有同學有自適應寬度更好的方法,歡迎不吝賜教唷!
都繫結好後,這個時候顯示肯定還是不正確的,因為現在TextContent是一個Json字串,所以我們還差一個Converter:
class ShowImageOrText : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
var v = JsonConvert.DeserializeObject<List<EmoticonText>>((string)value);
StringBuilder sb = new StringBuilder();
foreach (var item in v)
{
if (item.Key=="text")
{
sb.Append("<Run>");
sb.Append(item.Value);
sb.Append("</Run>");
}
else
{
sb.Append("<Image Width=\"32\" Source=\"");
sb.Append(item.Value);
sb.Append("\"/>");
}
}
return @"<Section xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation"" FontFamily=""Microsoft YaHei"" xml:space=""preserve"" TextAlignment=""Left"" LineHeight=""Auto""><Paragraph>" + sb.ToString() + "</Paragraph></Section>";
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
這段程式碼一看就懂,如果是文字則用run,如果是表情圖片,則用image,最後將拼裝好的xaml繫結到前端。
下來問題就出現了,當run裡面是中文時,前端會顯示為???(幾個中文就幾個?),也就是XamlFormatter並不能正確解析中文,哦到開~嘗試了修改各種Language屬性以及xml:lang=”en-us”,都是徒勞- -!
根據官網(http://wpftoolkit.codeplex.com/wikipage?title=RichTextBox)介紹,我們是可以自定義formater的,那到底怎麼自定義呢,在看了xctk:RichTextBox針對xaml的formatter這塊的原始碼後才明白,
其編碼格式用的ASCII,我們只要將其換成UTF8即可:
class RTBXamlFormatter : ITextFormatter
{
public string GetText(System.Windows.Documents.FlowDocument document)
{
TextRange tr = new TextRange(document.ContentStart, document.ContentEnd);
using (MemoryStream ms = new MemoryStream())
{
tr.Save(ms, DataFormats.Xaml);
return ASCIIEncoding.Default.GetString(ms.ToArray());
}
}
public void SetText(System.Windows.Documents.FlowDocument document, string text)
{
try
{
if (String.IsNullOrEmpty(text))
{
document.Blocks.Clear();
}
else
{
TextRange tr = new TextRange(document.ContentStart, document.ContentEnd);
using (MemoryStream ms = new MemoryStream(Encoding.**UTF8**.GetBytes(text)))
{
tr.Load(ms, DataFormats.Xaml);
}
}
}
catch
{
throw new InvalidDataException("Data provided is not in the correct Xaml format.");
}
}
}
記得將DataTemplate中的 <xctk:XamlFormatter/>
換成當前這個 <local:RTBXamlFormatter/>
-:)
以上2017-06-06
————————————————————————————————————————————————————
以下編輯於2017-10-25
看到有小夥伴在評論區提問,我這邊就再更新一下吧。
在之前的版本上我又做了以下更改:
1.修改richtextbox寬度計算方法
2.修改GenerateTextMessage方法
3.修改richtextbox顯示的TextFormatter
針對第1點,上面已經講了,如果是純文字或英文的話,寬度計算誤差會比較大,導致介面比較醜,然後在網上找到了一個計算文字長度的方法,並加以整合:
public static double CalcMessageWidth(Xceed.Wpf.Toolkit.RichTextBox t, double w)
{
TextRange range = new TextRange(t.Document.ContentStart, t.Document.ContentEnd);
var text = range.Text;
var formatText = GetFormattedText(t.Document);
int count = SubstringCount(t.Text, "pict") / 2;
return Math.Min(formatText.WidthIncludingTrailingWhitespace + 18 + count * 32, w);
}
public static FormattedText GetFormattedText(FlowDocument doc)
{
var output = new FormattedText(
GetText(doc),
System.Globalization.CultureInfo.CurrentCulture,
doc.FlowDirection,
new Typeface(doc.FontFamily, doc.FontStyle, doc.FontWeight, doc.FontStretch),
doc.FontSize,
doc.Foreground);
int offset = 0;
foreach (TextElement textElement in GetRunsAndParagraphs(doc))
{
var run = textElement as Run;
if (run != null)
{
int count = run.Text.Length;
output.SetFontFamily(run.FontFamily, offset, count);
output.SetFontSize(run.FontSize, offset, count);
output.SetFontStretch(run.FontStretch, offset, count);
output.SetFontStyle(run.FontStyle, offset, count);
output.SetFontWeight(run.FontWeight, offset, count);
output.SetForegroundBrush(run.Foreground, offset, count);
output.SetTextDecorations(run.TextDecorations, offset, count);
offset += count;
}
else
{
offset += Environment.NewLine.Length;
}
}
return output;
}
private static string GetText(FlowDocument doc)
{
var sb = new StringBuilder();
foreach (TextElement text in GetRunsAndParagraphs(doc))
{
var run = text as Run;
sb.Append(run == null ? Environment.NewLine : run.Text);
}
return sb.ToString();
}
private static IEnumerable<TextElement> GetRunsAndParagraphs(FlowDocument doc)
{
for (TextPointer position = doc.ContentStart;
position != null && position.CompareTo(doc.ContentEnd) <= 0;
position = position.GetNextContextPosition(LogicalDirection.Forward))
{
if (position.GetPointerContext(LogicalDirection.Forward) == TextPointerContext.ElementEnd)
{
var run = position.Parent as Run;
if (run != null)
{
yield return run;
}
else
{
var para = position.Parent as Paragraph;
if (para != null)
{
yield return para;
}
else
{
var lineBreak = position.Parent as LineBreak;
if (lineBreak != null)
{
yield return lineBreak;
}
}
}
}
}
}
public static int SubstringCount(string str, string substring)
{
if (str.Contains(substring))
{
string strReplaced = str.Replace(substring, "");
return (str.Length - strReplaced.Length) / substring.Length;
}
return 0;
}
在richtextbox的TextChanged事件裡呼叫以上CalcMessageWidth(第二個引數是你設定的訊息最大寬度)方法就可以算出訊息寬度了,再也不怕純英文或數字了,但是這個方法也有些弊端:
a.只能計算text長度,不包含圖片,於是我加了個SubstringCount(t.Text, “pict”) / 2方法來計算訊息中表情的個數,並且加上了每個表情32的寬度。t.Textd得到的是richtextbox內容的rtf格式,裡面pict代表圖片。
b.由於是在TextChanged事件裡呼叫的,所以每發或收一條訊息,之前所有的訊息都會觸發,這樣勢必會多消耗一些資源。
針對第2點,既然已經用了特定的方法來計算寬度,那麼GenerateTextMessage方法裡的計算就可以去掉了:
private VText GenerateTextMessage(NIMTextMessage m)
{
VText text = new VText();
var txt = m.TextContent;
if (txt.Contains("[") && txt.Contains("]"))
{
List<EmoticonText> emoticonText = new List<EmoticonText>();
char[] chars = txt.ToCharArray();
for (int i = 0; i < chars.Length; i++)
{
char c = chars[i];
if (chars[i] == '[')
{
int f = txt.IndexOf(']', i);
if (f < 0)
{
emoticonText.Add(new EmoticonText { Key = "text", Value = c.ToString() });
}
else
{
string es = txt.Substring(i, f - i + 1);
XElement node = elementCollection.Where(a => a.Attribute("Tag").Value == es).FirstOrDefault();
if (node == null)
{
emoticonText.Add(new EmoticonText { Key = "text", Value = c.ToString() });
}
else
{
emoticonText.Add(new EmoticonText { Key = "emoticon", Value = "../Resources/Emoticon/" + node.Attribute("File").Value });
i = f;
}
}
}
else
{
emoticonText.Add(new EmoticonText { Key = "text", Value = c.ToString() });
var emoticonWord = emoticonText.Where(p => p.Value == "\r").FirstOrDefault();
emoticonText.Remove(emoticonWord);
}
}
text.TextContent = JsonConvert.SerializeObject(emoticonText);
}
else
{
List<EmoticonText> textStr = new List<EmoticonText>();
textStr.Add(new EmoticonText() { Key = "text", Value = txt });
text.TextContent = JsonConvert.SerializeObject(textStr);
}
return text;
}
有小夥伴問EmoticonText實體,它其實就是個key-value,跟converter裡配套使用的:
public class EmoticonText
{
public string Key { get; set; }
public string Value { get; set; }
}
針對第3點,之前是用xctk:XamlFormatter,發現還是有不少問題,於是就採用了xctk:RtfFormatter
<Border CornerRadius="8" Background="#F0F0F0" Padding="6" HorizontalAlignment="Left" Margin="0 4 0 0">
<xctk:RichTextBox VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Disabled"
Text="{Binding TextContent,Converter={StaticResource ShowImageOrTextConverter}}"
VerticalAlignment="Center"
BorderThickness="0"
IsReadOnly="True"
Background="Transparent"
TextChanged="RichTextBox_TextChanged_1">
<xctk:RichTextBox.TextFormatter>
<xctk:RtfFormatter />
</xctk:RichTextBox.TextFormatter>
</xctk:RichTextBox>
</Border>
既然換了xctk:RtfFormatter,那麼繫結給Text的資料也要變了,修改converter:
class ShowImageOrText : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
var v = JsonConvert.DeserializeObject<List<EmoticonText>>((string)value);
StringBuilder sb = new StringBuilder();
foreach (var item in v)
{
if (item.Key == "text")
{
sb.Append("<Run>");
sb.Append(item.Value.Replace("<", "LessSymbol").Replace("\r\n", "</Run><LineBreak/><Run>").Replace("\n", "</Run><LineBreak/><Run>"));
sb.Append("</Run>");
}
else
{
sb.Append("<Image Width=\"32\" Source=\"");
sb.Append(item.Value);
sb.Append("\"/>");
}
}
var str = @"<Section xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation"" FontFamily=""Microsoft YaHei"" FontSize=""14"" xml:space=""preserve"" TextAlignment=""Left"" LineHeight=""Auto""><Paragraph>" + sb.ToString() + "</Paragraph></Section>";
return ConvertXamlToRtf(str);
}
/// <summary>
/// https://code.msdn.microsoft.com/windowsdesktop/Converting-between-RTF-and-aaa02a6e
/// </summary>
/// <param name="xamlText"></param>
/// <returns></returns>
private static string ConvertXamlToRtf(string xamlText)
{
var richTextBox = new RichTextBox();
if (string.IsNullOrEmpty(xamlText)) return "";
var textRange = new TextRange(richTextBox.Document.ContentStart, richTextBox.Document.ContentEnd);
try
{
using (var xamlMemoryStream = new MemoryStream())
{
using (var xamlStreamWriter = new StreamWriter(xamlMemoryStream))
{
xamlStreamWriter.Write(xamlText.Replace("&", "AndSymbol"));
xamlStreamWriter.Flush();
xamlMemoryStream.Seek(0, SeekOrigin.Begin);
textRange.Load(xamlMemoryStream, DataFormats.Xaml);
}
}
}
catch (Exception)
{
var str = @"<Section xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation"" FontFamily=""Microsoft YaHei"" FontSize=""14"" xml:space=""preserve"" TextAlignment=""Left"" LineHeight=""Auto""><Paragraph><Run>該資訊包含特殊字元,無法顯示</Run></Paragraph></Section>";
using (var xamlMemoryStream = new MemoryStream())
{
using (var xamlStreamWriter = new StreamWriter(xamlMemoryStream))
{
xamlStreamWriter.Write(str);
xamlStreamWriter.Flush();
xamlMemoryStream.Seek(0, SeekOrigin.Begin);
textRange.Load(xamlMemoryStream, DataFormats.Xaml);
}
}
}
using (var rtfMemoryStream = new MemoryStream())
{
textRange = new TextRange(richTextBox.Document.ContentStart, richTextBox.Document.ContentEnd);
textRange.Save(rtfMemoryStream, DataFormats.Rtf);
rtfMemoryStream.Seek(0, SeekOrigin.Begin);
using (var rtfStreamReader = new StreamReader(rtfMemoryStream))
{
return rtfStreamReader.ReadToEnd().Replace("AndSymbol", "&").Replace("LessSymbol", "<");
}
}
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
這個converter將xaml轉成rtf再繫結給richtextbox的Text,並且針對一些特殊字元做了特殊處理以及異常處理,小夥伴們使用時看情況修改~
好了,以上基本就是用到的所有方法了,也算是給原始碼了。
再上個圖吧-:)