WPF实现带滚动字幕的浮窗
1、形式
做这个东西时我只是单纯觉得有趣,而且也能进行近期工作的提醒,做完以后很长时间没有改动,现在看来还是有许多不足的。先看一下窗口的具体样式
文本设计的具体文档如下:
<Window x:Class="WpfApp2.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApp2"
mc:Ignorable="d"
Title="MainWindow" Height="184" Width="829" WindowStartupLocation="Manual" WindowStyle="None" BorderBrush="White" MouseDown="Window_MouseDown" MouseMove="Window_MouseMove" AllowsTransparency="True" Loaded="Window_Loaded" HorizontalAlignment="Center" VerticalAlignment="Top" Top="-3" Left="700">
<Window.Background>
<SolidColorBrush Color="White" Opacity="0.5"/>
</Window.Background>
<Grid MouseDown="Grid_MouseDown" MouseMove="Grid_MouseMove" Opacity="0.5">
<Grid.RowDefinitions>
<RowDefinition Height="19*"/>
<RowDefinition Height="131*"/>
<RowDefinition Height="34*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="114*"/>
<ColumnDefinition Width="611*"/>
<ColumnDefinition Width="104*"/>
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="3" Orientation="Horizontal" Margin="10,0,-2,0">
<Button Content="" Width="19" Click="Button_Click_3">
<Button.Background>
<ImageBrush ImageSource="zuixiaoh.png"/>
</Button.Background>
</Button>
<Button x:Name="Button_stop" Content="" Width="19" Click="Button_stop_Click">
<Button.Background>
<ImageBrush ImageSource="renew.png"/>
</Button.Background>
</Button>
<Button Content="
" Width="19" Click="Button_Click_2">
<Button.Background>
<ImageBrush ImageSource="STOp.png">
<ImageBrush.RelativeTransform>
<TransformGroup>
<ScaleTransform CenterY="0.5" CenterX="0.5" ScaleX="0.9" ScaleY="0.9"/>
<SkewTransform CenterY="0.5" CenterX="0.5"/>
<RotateTransform CenterY="0.5" CenterX="0.5"/>
<TranslateTransform/>
</TransformGroup>
</ImageBrush.RelativeTransform>
</ImageBrush>
</Button.Background>
</Button>
<Button Content="" HorizontalAlignment="Right" Width="19" Click="Button_Click_1" Foreground="{x:Null}" Height="19" VerticalAlignment="Bottom">
<Button.Background>
<ImageBrush ImageSource="2.png">
<ImageBrush.RelativeTransform>
<TransformGroup>
<ScaleTransform CenterY="0.5" CenterX="0.5" ScaleX="1.1" ScaleY="1.05"/>
<SkewTransform CenterY="0.5" CenterX="0.5"/>
<RotateTransform CenterY="0.5" CenterX="0.5"/>
<TranslateTransform/>
</TransformGroup>
</ImageBrush.RelativeTransform>
</ImageBrush>
</Button.Background>
</Button>
<Button Content="" Click="Button_Click" Width="18">
<Button.Background>
<ImageBrush ImageSource="3.png"/>
</Button.Background>
</Button>
</StackPanel>
<TextBlock x:Name="TB_Content" Grid.Column="2" Grid.Row="1" TextWrapping="Wrap" Background="#FF04F71A" FontSize="24"/>
<TextBlock x:Name="TB_Content1" Grid.Column="2" Grid.Row="2" TextWrapping="Wrap"/>
<Image Grid.ColumnSpan="2" Grid.Row="1" Margin="0,0,0,20" Source="11.png"/>
</Grid>
</Window>
在窗体属性 外观项中设置WindowStyle为None,然后根据需要设置各个按键即可。
2、具体实现
(1)窗体移动
注意一点,当设置WindowStyle为None时,用鼠标是不能移动窗体的,为了实现窗体移动,需要定义鼠标事件Window_MouseDown和Window_MouseMove,注意不要写Grid_MouseDown和Grid_MouseMove
首先定义鼠标与窗体左上角的距离`
private void Mouse_Procedure(Point Mouse_Postion)
{
this.Left += Mouse_Postion.X - FormerX;
this.Top += Mouse_Postion.Y - FormerY;
}
之后定义Grid_MouseDown和Grid_MouseMove事件
private void Window_MouseDown(object sender, MouseButtonEventArgs e)
{
FormerX = e.GetPosition(this).X;
FormerY = e.GetPosition(this).Y;
}
private void Window_MouseMove(object sender, MouseEventArgs e)
{
if (e.LeftButton == MouseButtonState.Pressed)
{
Mouse_Procedure(Mouse.GetPosition(this));
}
if (this.Top <= 0)
this.Top = 0;
}
用鼠标按下时距窗口的距离初始化FormerX和FormerY,并在移动过程中保持FormerX和FormerY不变。
同时可以用MouseUp事件来使窗口自动隐藏,实现比较简单,就不贴代码了。
(2)文字输出
这是重头戏,在这里我采用了task来保证窗口响应
主体代码如下:
public MainWindow()
{
InitializeComponent();
if (File.Exists("Settings.txt"))
{
// SetBinding("Settings.txt");
// FileShow("Slogan.txt");
this.Top = 0;
}
else
MessageBox.Show("Check the completion");
if (File.Exists("Slogan.txt"))
{
FileShow("Slogan.txt");
this.Top = 0;
}
TimeShow();
}
为了实现滚动字体,定义了FileShow函数,用于读取文件并显示,在其中异步读取文件流,并通过Move函数实现滚动字幕
async private void FileShow(string FilePath)
{
Stream StringStream = new FileStream(FilePath, FileMode.Open);
StreamReader TextRead = new StreamReader(StringStream, Encoding.Default);
string[] Target = (await TextRead.ReadToEndAsync()).Split('\r');
await Move(Target);
TextRead.Dispose();
StringStream.Dispose();
}
move函数定义如下:
async Task Move(string[] Target)
{
int i = 0;
while (status)
{
TB_Content.Text = "";
TB_Move(Target[i].Trim(new char[] { '\n', ' ' }));
await Task.Delay(Target[i].Length * 100 + 1000);
if (i < Target.Count() - 1)
i++;
else
i = 0;
}
// return Task.CompletedTask;
return;
}
在其中status为全局变量,用于控制该代码块的停止,利用 TB_Move来控制滚动字幕,i为控制循环播放的变量,await Task.Delay(Target[i].Length * 100 + 1000);是必要的,否则就会出现前后字幕冲突,1000为两段字幕之间的间隔时间
TB_Move定义如下
async private void TB_Move(string ShowString)
{
for (int i = 0; i < ShowString.Length; i++)
{
await Task.Delay(100);
if (i % count != 0)
TB_Content.Text += ShowString[i];
else if (i % count == 0 && i != 0)
TB_Content.Text += "\r\n" + ShowString[i];
else
TB_Content.Text += ShowString[i];
}
}
await Task.Delay(100);中的100为一个字出现的延时时间,现在定为0.1s,可以自行修改。
注意一点,我在这里把字幕文字文件设为"Slogan.txt",文件中用回车符作为两段之间的间隔符
此外还加入了倒计时的功能,具体以 TimeShow函数实现:
async private void TimeShow()
{
Stream StringStream = new FileStream("countdown.txt", FileMode.Open);
StreamReader TextRead = new StreamReader(StringStream, Encoding.Default);
string Title= await TextRead.ReadLineAsync();
DateTime Enddate =DateTime.Parse((await TextRead.ReadLineAsync()));
await TimeExcuete(Title,Enddate);
TextRead.Dispose();
StringStream.Dispose();
}
TimeExcuete进行控制:
async Task TimeExcuete(string Title,DateTime EndDate)
{
while (status)
{
TB_Content1.Text = Title+":" + (EndDate - DateTime.Now).ToString(@"dddd\-hh\:mm\:ss\.ff");
await Task.Delay(10);
}
return;
}
文件存储于"countdown.txt",第一行为标题,第二行为截止日期
附加功能
(1)关闭、最小化窗体,很简单,不做说明
(2)停止与刷新:
停止:
private void Button_Click_2(object sender, RoutedEventArgs e)
{
status = !status;
}
刷新:
private void Button_stop_Click(object sender, RoutedEventArgs e)
{
status = true;
FileShow("Slogan.txt");
TimeShow();
}
如果想要在之前的位置进行读取,将Move函数里的i改为全局变量即可。
附录
现在把完整代码附在最后,如果要把窗口设置为总在最前,将公共中的topMost属性勾选即可
这个代码是不含自动隐藏和显示的,如果要实现功能还需进一步开发。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.IO;
namespace WpfApp2
{
/// <summary>
/// MainWindow.xaml 的交互逻辑
/// </summary>
public partial class MainWindow : Window
{
private double FormerX = 0;
private double FormerY = 0;
bool status = true;
int count = 0;
public MainWindow()
{
InitializeComponent();
if (File.Exists("Settings.txt"))
{
// SetBinding("Settings.txt");
// FileShow("Slogan.txt");
this.Top = 0;
}
else
MessageBox.Show("Check the completion");
if (File.Exists("Slogan.txt"))
{
FileShow("Slogan.txt");
this.Top = 0;
}
TimeShow();
}
async private void SetBinding(string FilePath)
{
Stream StringStream = new FileStream(FilePath, FileMode.Open);
StreamReader TextRead = new StreamReader(StringStream, Encoding.Default);
}
async private void FileShow(string FilePath)
{
Stream StringStream = new FileStream(FilePath, FileMode.Open);
StreamReader TextRead = new StreamReader(StringStream, Encoding.Default);
string[] Target = (await TextRead.ReadToEndAsync()).Split('\r');
await Move(Target);
TextRead.Dispose();
StringStream.Dispose();
}
async Task Move(string[] Target)
{
int i = 0;
while (status)
{
TB_Content.Text = "";
TB_Move(Target[i].Trim(new char[] { '\n', ' ' }));
await Task.Delay(Target[i].Length * 100 + 1000);
if (i < Target.Count() - 1)
i++;
else
i = 0;
}
// return Task.CompletedTask;
return;
}
async private void TimeShow()
{
Stream StringStream = new FileStream("countdown.txt", FileMode.Open);
StreamReader TextRead = new StreamReader(StringStream, Encoding.Default);
string Title= await TextRead.ReadLineAsync();
DateTime Enddate =DateTime.Parse((await TextRead.ReadLineAsync()));
await TimeExcuete(Title,Enddate);
TextRead.Dispose();
StringStream.Dispose();
}
async Task TimeExcuete(string Title,DateTime EndDate)
{
while (status)
{
TB_Content1.Text = Title+":" + (EndDate - DateTime.Now).ToString(@"dddd\-hh\:mm\:ss\.ff");
await Task.Delay(10);
}
return;
}
async private void TB_Move(string ShowString)
{
for (int i = 0; i < ShowString.Length; i++)
{
await Task.Delay(100);
if (i % count != 0)
TB_Content.Text += ShowString[i];
else if (i % count == 0 && i != 0)
TB_Content.Text += "\r\n" + ShowString[i];
else
TB_Content.Text += ShowString[i];
}
}
private void Mouse_Procedure(Point Mouse_Postion)
{
this.Left += Mouse_Postion.X - FormerX;
this.Top += Mouse_Postion.Y - FormerY;
}
private void Button_Click(object sender, RoutedEventArgs e)
{
this.Close();
}
private void Grid_MouseDown(object sender, MouseButtonEventArgs e)
{
}
private void Grid_MouseMove(object sender, MouseEventArgs e)
{
}
private void Window_MouseDown(object sender, MouseButtonEventArgs e)
{
FormerX = e.GetPosition(this).X;
FormerY = e.GetPosition(this).Y;
}
private void Window_MouseMove(object sender, MouseEventArgs e)
{
if (e.LeftButton == MouseButtonState.Pressed)
{
Mouse_Procedure(Mouse.GetPosition(this));
}
if (this.Top <= 0)
this.Top = 0;
}
private void Button_Click_1(object sender, RoutedEventArgs e)
{
MessageBox.Show("Settings");
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
count = (int)(TB_Content.ActualWidth / TB_Content.FontSize);
}
private void Button_Click_2(object sender, RoutedEventArgs e)
{
status = !status;
}
private void Button_stop_Click(object sender, RoutedEventArgs e)
{
status = true;
FileShow("Slogan.txt");
TimeShow();
}
private void Button_Click_3(object sender, RoutedEventArgs e)
{
this.WindowState = WindowState.Minimized;
}
}
}