65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 Prism 的可扩展屏幕保护程序

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2投票s)

2017年1月26日

CPOL

9分钟阅读

viewsIcon

9039

downloadIcon

332

在 WPF 中使用 Prism 模式编写的屏幕保护程序应用程序。

引言

本文介绍了一个使用 WPF 和 Prism 模式编写的屏幕保护程序应用程序。提供的代码是一个完整的应用程序,其中包含两种不同的动画模式,这两种模式在两个模块中实现。该应用程序是模块化的,可以通过添加新模块轻松扩展。

特点

该应用程序展示了以下功能:

  1. 可以作为屏幕保护程序安装
  2. 支持辅助显示器
  3. 包含两个动画模块:闪烁条纹和动画网格
  4. 用户可以动态切换模块
  5. 用户可以重新启动动画并更改模块设置
  6. 设置保存为 XML 文件
  7. 使用 log4net 库进行日志记录

背景

该解决方案使用 C#6、.NET 4.5.1、WPF(带 Prism 模式)、NuGet 包 Extended.Wpf.ToolkitIkc5.Prism.SettingsIkc5.TypeLibrary

屏幕保护程序

First module

屏幕保护程序是一个普通的 GUI 应用程序,但具有特殊的启动方式。它应该能够接受输入参数并在三种模式下执行:显示、预览和配置。
有一些文章描述了 WPF 屏幕保护程序:

使用 Prism 模式可以创建一个模块化应用程序,其中动画模块可以轻松添加并动态切换。模块中的视图实现 IActiveAware 接口,该接口允许控制视图的活动并执行命令。模块和 WPF 应用程序具有包含大小、颜色、迭代延迟的设置。该应用程序使用 Examples of using Ikc5.Prism.Settings 中描述的 Ikc5.Prism.Settings 包,并将应用程序和模块的设置保存到 %AppData% 文件夹中的 XML 文件中。

解决方案

该解决方案具有以下结构:

  1. Common - Common.Models 包含枚举、模型类和接口;Common.ViewModels 包含附加属性、转换器、样式和视图模型的层次结构
  2. 第一个模块 - FirstModule.Models 包含设置类和模型类;FirstModule.Views 包含模块类、视图和视图模型
  3. 第二个模块 - 结构与第一个模块相同
  4. ScreenSaver - 主 WPF 应用程序

通用类库

动画模块基于 Grid with dynamic number of rows and columns, part 2 中描述的动态网格。该文章描述了 WPF datagrid,其单元格具有固定的定义大小,但行数和列数动态更新以填充所有可用空间。此处及以下,我们引用该代码中的类。

Common.Models 包含 ICell 接口、CellCellSet 类。Cell 未更改,具有一个布尔属性,单元格集获得一个额外的用于迭代的 InvertPoint 方法。

public void InvertPoints(IEnumerable<Point> newPoints)
{
    if (newPoints == null)
        return;
    foreach (var point in newPoints)
    {
        Cells[point.X, point.Y].State = !Cells[point.X, point.Y].State;
    }
}

Common.Models 包含 IDynamicGridViewModelIBaseCellViewModel 接口、设计和基视图模型。IBaseCellViewModel 由显示单元格模型的视图使用,并且可以在模块中通过颜色等附加属性进行继承和扩展。它很简单。

public interface IBaseCellViewModel
{
    /// <summary>
    /// Cell model.
    /// </summary>
    ICell Cell { get; set; }
}

IDynamicGridViewModel 接口通过 iterate 命令和 IActiveAware 接口进行了扩展。

public interface IDynamicGridViewModel<TCellViewModel> : 
                         IActiveAware where TCellViewModel : IBaseCellViewModel
{
    /// <summary>
    /// Width of current view - expected to be bound to view's actual
    /// width in OneWay binding.
    /// </summary>
    int ViewWidth { get; set; }

    /// <summary>
    /// Height of current view - expected to be bound to view's actual
    /// height in OneWay binding.
    /// </summary>
    int ViewHeight { get; set; }

    /// <summary>
    /// Width of the cell.
    /// </summary>
    int CellWidth { get; set; }

    /// <summary>
    /// Height of the cell.
    /// </summary>
    int CellHeight { get; set; }

    /// <summary>
    /// Count of grid columns.
    /// </summary>
    int GridWidth { get; }

    /// <summary>
    /// Count of grid rows.
    /// </summary>
    int GridHeight { get; }

    /// <summary>
    /// Data model.
    /// </summary>
    CellSet CellSet { get; }

    /// <summary>
    /// 2-dimensional collections for CellViewModels.
    /// </summary>
    ObservableCollection<ObservableCollection<TCellViewModel>> Cells { get; }

    /// <summary>
    /// Iterates screen saver at one cycle.
    /// </summary>
    ICommand IterateCommand { get; }

    /// <summary>
    /// Command starts iterating.
    /// </summary>
    ICommand StartIteratingCommand { get; }

    /// <summary>
    /// Command stops iterating.
    /// </summary>
    ICommand StopIteratingCommand { get; }

    /// <summary>
    /// Set new initial set of points.
    /// </summary>
    ICommand RestartCommand { get; }
}

接口的实现是一个 abstract DynamicGridViewModel 类。它保留了上述文章中的代码,实现了 IActiveAware 并通过迭代方法和命令进行了扩展。IActiveAware 接口的实现和目标在 Detecting the Active View in a Prism App 中有描述。

视图模型包含迭代计时器。

_iterateTimer = new DispatcherTimer
{
    Interval = TimeSpan.FromMilliseconds(IterationDelay),
};
_iterateTimer.Tick += IterateTimerTick;

当计时器嘀嗒时,调用 IterateTimerTick 方法,其中随机生成的单元格集反转状态。

private void IterateTimerTick(object sender, EventArgs e)
{
    Iterate();
}

private void Iterate()
{
    if (CellSet == null)
        return;

    using (Application.Current.Dispatcher.DisableProcessing())
    {
        var points = GenerateRandomPoints(11);
        CellSet.InvertPoints(points);
    }
}

此外,如果由于视图大小更改而重新创建单元格集,迭代计时器应暂停然后继续。因此,类包含 StartTimerStopTimerPauseIterationContinueIteration 等方法,并具有自然实现。

protected void PauseIteration()
{
    if (_iterateTimer.IsEnabled)
    {
        _postponedTimer = true;
        _iterateTimer.Stop();
    }
}

protected void ContinueIteration()
{
    if (_postponedTimer)
        StartTimer();
}

/// <summary>
/// Start the timer for screen saver iterations. But method could have not effect
/// if cell set still waits for all necessary data for creating. Then timer will
/// start after cell set has been created.
/// </summary>
private void StartTimer()
{
    if (CellSet == null || Cells == null)
        _postponedTimer = true;
    else
    {
        _iterateTimer.Start();
        _postponedTimer = false;
        SetCommandProviderMode(CommandProviderMode.Iterating);
    }
}

/// <summary>
/// Stop iteration timer.
/// </summary>
private void StopTimer()
{
    _iterateTimer.Stop();
    SetCommandProviderMode(CommandProviderMode.Init);
}

模块

该应用程序包含两个模块。第一个模块显示具有两种颜色的网格单元格:如果 State 等于 true,则为 StartColor;否则为 FinishColor。第二个模块显示带有渐变填充的垂直条纹,与 State 等于 true 的单元格模型对应。动画由 CellView 类实现。

<Grid x:Name="MainPanel">
    <Border
        BorderThickness="1"
        BorderBrush="{Binding BorderColor,
                RelativeSource={RelativeSource FindAncestor, 
                AncestorType={x:Type UserControl}},
                Converter={StaticResource ColorToBrushConverter},
                FallbackValue=#FF000000}"
        Background="{Binding StartColor,
                RelativeSource={RelativeSource FindAncestor, 
                AncestorType={x:Type UserControl}},
                Converter={StaticResource ColorToBrushConverter},
                FallbackValue=#FF40FF40}">

        <Border
            BorderThickness="0"
            Background="{Binding FinishColor,
                            RelativeSource={RelativeSource FindAncestor, 
                            AncestorType={x:Type UserControl}},
                            Converter={StaticResource ColorToBrushConverter},
                            FallbackValue=#FFFF4040}"
            Visibility="{Binding Path=Cell.State, Mode=OneWay, 
                        Converter={StaticResource BooleanToVisibilityConverter}, 
                        FallbackValue=Hidden}"
            attached:VisibilityAnimation.AnimationType=
                        "{Binding Path=Settings.AnimationType, Mode=OneWay}" 
            attached:VisibilityAnimation.AnimationDuration=
                        "{Binding Path=Settings.AnimationDelay, Mode=OneWay}"/>
    </Border>
</Grid>
<Grid x:Name="MainPanel">
    <Border
        BorderThickness="0"
        Background="Transparent">

        <Border
            BorderThickness="1"
            BorderBrush="{Binding BorderColor,
                    RelativeSource={RelativeSource FindAncestor, 
                                    AncestorType={x:Type UserControl}},
                    Converter={StaticResource ColorToBrushConverter},
                    FallbackValue=#FF000000}"
            Visibility="{Binding Path=Cell.State, Mode=OneWay, 
                        Converter={StaticResource BooleanToVisibilityConverter}, 
                                   FallbackValue=Visible}"
            attached:VisibilityAnimation.AnimationType=
                                 "{Binding Path=Settings.AnimationType, Mode=OneWay}" 
            attached:VisibilityAnimation.AnimationDuration=
                                 "{Binding Path=Settings.AnimationDelay, Mode=OneWay}">
                
            <Border.Background>
                <LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
                    <GradientStop Color="{Binding StartColor,
                                        RelativeSource={RelativeSource FindAncestor, 
                                        AncestorType={x:Type UserControl}},
                                        FallbackValue=#FF40FF40}" Offset="0"/>
                    <GradientStop Color="{Binding FinishColor,
                                        RelativeSource={RelativeSource FindAncestor, 
                                        AncestorType={x:Type UserControl}},
                                        FallbackValue=#FFFF4040}" Offset="1"/>
                </LinearGradientBrush>
            </Border.Background>
        </Border>
    </Border>
</Grid>

模块以类似的方式构建,所以让我们考虑其中一个。FirstModule.Models 类库包含 ISettings 接口,该接口列出了模块的设置:颜色、大小和延迟,以及该接口的默认实现 - Settings 类。FirstModule.Views 包含 MainViewSettingsViewCellView 视图以及相应的视图模型。MainView 代码与 所述文章 中描述的相同。SettingsView 在应用程序的 SettingsWindow 窗口区域中使用,并向用户提供当前设置以及更改它们的可能性。

IMainViewModelICellViewModel 接口分别派生自 IDynamicGridViewModelIBaseCellViewModel。派生接口包含 ISettings 实例,允许根据模块设置形成表示。

此外,MainViewModel 视图模型派生自 DynamicGridViewModel,并且只实现 ISettings 属性和 PropertyChanged 事件的订阅者。

private ISettings _settings;

public ISettings Settings
{
    get { return _settings; }
    private set
    {
        var userSettings = _settings as IUserSettings;
        if (userSettings != null)
            userSettings.PropertyChanged -= UserSettingsOnPropertyChanged;

        SetProperty(ref _settings, value);
        userSettings = _settings as IUserSettings;
        if (userSettings != null)
            userSettings.PropertyChanged += UserSettingsOnPropertyChanged;
    }
}

private void UserSettingsOnPropertyChanged(object sender, PropertyChangedEventArgs args)
{
    if (CellSet == null)
        return;

    PauseIteration();

    switch (args.PropertyName)
    {
    case nameof(Settings.CellWidth):
        CellWidth = Settings.CellWidth;
        break;

    case nameof(Settings.CellHeight):
        CellHeight = Settings.CellHeight;
        break;

    case nameof(Settings.IterationDelay):
        IterationDelay = Settings.IterationDelay;
        break;

    default:
        break;
    }

    ContinueIteration();
}

模块之间的相互关系

Prism 模式的原则之一是模块的独立性,但应用程序需要与模块交互。在此示例中,主窗口显示一个包含“重新启动”项的上下文菜单。它需要找到活动视图并重新启动其动画。此任务通过使用 CompositeCommandDelegateCommand 来解决。

模块主视图实现了 Prism 基础设施支持的 IActiveAware 接口。然后,视图将调用传递给视图模型和命令,视图模型知道模块的活动。

通用库包含 ICommandProvider 接口

public interface ICommandProvider : INotifyPropertyChanged
{
    CompositeCommand IterateCommand { get; }
    CompositeCommand StartIteratingCommand { get; }
    CompositeCommand StopIteratingCommand { get; }
    CompositeCommand RestartCommand { get; }
}

在主应用程序中,此接口由 CommandProvider 类实现,其中复合命令是在了解活动模块的情况下创建的。

public class CommandProvider : BindableBase, ICommandProvider
{
    public CompositeCommand IterateCommand { get; }
        = new CompositeCommand(monitorCommandActivity: true);
    public CompositeCommand StartIteratingCommand { get; }
        = new CompositeCommand(monitorCommandActivity: true);
    public CompositeCommand StopIteratingCommand { get; }
        = new CompositeCommand(monitorCommandActivity: true);
    public CompositeCommand RestartCommand { get; }
        = new CompositeCommand(monitorCommandActivity: true);
}

每个模块中主视图的视图模型创建委托命令并注册它们。

// create commands
IterateCommand = new DelegateCommand(Iterate, () => CanIterate)
    { IsActive = IsActive };
StartIteratingCommand = new DelegateCommand(StartTimer, () => CanStartIterating)
    { IsActive = IsActive };
StopIteratingCommand = new DelegateCommand(StopTimer, () => CanStopIterating)
    { IsActive = IsActive };
RestartCommand = new DelegateCommand(Restart, () => CanRestart)
    { IsActive = IsActive };

// register command in composite commands
commandProvider.IterateCommand.RegisterCommand(IterateCommand);
commandProvider.StartIteratingCommand.RegisterCommand(StartIteratingCommand);
commandProvider.StopIteratingCommand.RegisterCommand(StopIteratingCommand);
commandProvider.RestartCommand.RegisterCommand(RestartCommand);

CanExecute 属性根据迭代和创建模型的不同状态进行更新。

添加新模块

如上所述,新模块可以轻松添加。应用程序向用户提供所有已注册模块的列表,并允许选择活动模块。设置窗口包含所有已注册模块的设置选项卡。因此,让我们考虑添加名为 ThirdModule 的模块的步骤:

  1. 创建新的类库 ThirdLibrary.ViewsThirdLibrary.Views,或复制 SecondModule.* 库并将其中的所有文件和类从“Second”重命名为“Third”。
  2. 更新 ISettings 接口中的属性以对应模块设置;更新所有派生类,如 SettingsDesignSettingsISettingsViewModel 接口、SettingsViewModel,并更新 SettingsView 视图中的元素和绑定。
  3. 在应用程序的区域中注册模块的视图
    public void Initialize()
    {
        _regionManager.RegisterViewWithRegion(PrismNames.MainRegionName, typeof(MainView));
        _regionManager.RegisterViewWithRegion($"{GetType().Name}
                       {RegionNames.ModuleSettingsRegion}", typeof(SettingsView));
    }
  4. Bootstrapper 类中注册模块
    protected override void ConfigureModuleCatalog()
    {
        var catalog = (ModuleCatalog)ModuleCatalog;
        // add all modules
        catalog.AddModule(typeof(FirstModule.FirstModule));
        catalog.AddModule(typeof(SecondModule.SecondModule));
        catalog.AddModule(typeof(ThirdModule.ThirdModule));
    }
  5. 如果模块动画基于动态网格,则只需更新 CellView 视图;否则,需要编写 MainViewCellView 视图的代码。

屏幕保护程序应用程序

WPF 应用程序包含视图、视图模型、引导程序和应用程序类。MainWindowEmptyWindow 是屏幕保护程序的主窗口。Settings 类和 SettingsWindowExamples of using Ikc5.Prism.Settings 中有描述。主窗口具有占用整个空间的主区域和上下文菜单。

<Grid x:Name="MainGrid"
        d:DataContext="{d:DesignInstance Type=viewModels:DesignMainWindowModel, 
                        IsDesignTimeCreatable=True}">
    <Grid.Background>
        <SolidColorBrush Color="{Binding Path=Settings.BackgroundColor, 
                                 Mode=OneWay, FallbackValue=#FFC0C0C0}"/>
    </Grid.Background>

    <Grid.ContextMenu>
        <ContextMenu> 
            <MenuItem Header="Restart"
                        Command="{Binding RestartCommand, Mode=OneWay}"
                        Click="MenuItem_OnClick"/>
            <Separator />
            <MenuItem Header="Settings"
                        Command="{Binding SettingsCommand, Mode=OneWay}"
                        Click="MenuItem_OnClick"/>
            <MenuItem Header="About"
                        Command="{Binding AboutCommand, Mode=OneWay}"
                        Click="MenuItem_OnClick"/>
        </ContextMenu>
    </Grid.ContextMenu>

    <ContentControl
        regions:RegionManager.RegionName="MainRegion"/>
</Grid>

Context menu

  1. Restart - 使用随机活动单元格集重新启动当前视图
  2. Settings - 显示设置窗口,允许用户在不停止屏幕保护程序的情况下更改设置
  3. About - 显示关于对话框

根据 Prism 模式,应用程序使用 Bootstrapper 类,其中执行所有必要的初始化,OnStartup 方法通常如下所示:

protected override void OnStartup(StartupEventArgs e)
{
    base.OnStartup(e);
    // create and launch bootstrapper
    var bootstrapper = new Bootstrapper();
    bootstrapper.Run();
}

对于屏幕保护程序应用程序,应在 OnStartup 方法中执行以下步骤:

  1. 创建引导程序,但不显示主窗口。
  2. 根据屏幕保护程序的启动类型设置 Shutdown 模式。
  3. 解析输入参数并选择应用程序的必要行为。
  4. 在显示模式下,为辅助显示器创建附加窗口。
  5. 在设置模式下,显示设置窗口并更正关闭模式。
  6. 在预览模式下,在 Win32 窗口中显示 WPF 窗口,并注意资源清理。

下面,我们将考虑这些步骤的实现。

初始化

主窗口在 Prism 中称为 Shell,通常在 CreateShell 方法中创建并在 InitializeShell 方法中初始化。

protected override DependencyObject CreateShell()
{
    Window mainWindow = Container.Resolve<MainWindow>();
    return mainWindow;
}

protected override void InitializeShell()
{
    var regionManager = Container.Resolve<IRegionManager>();
    // add some views to region adapter
    // ...

    // show window
    Application.Current.MainWindow.Show();
}

在屏幕保护程序中,主窗口应完全创建,但保持隐藏,因为屏幕保护程序可以以预览或配置模式启动。因此,InitializeShell 不会显示主窗口。

protected override void InitializeShell()
{
    var regionManager = Container.Resolve<IRegionManager>();
    // add some views to region adapter
    // ...

    // don't show window now - application may runs in settings mode
    // Application.Current.MainWindow.Show();
}

如果应用程序以显示模式运行,它只需在 OnStartup 模式下显示主窗口。

Application.Current.MainWindow.Show();

输入参数

所有屏幕保护程序都需要处理以下命令行参数:

  1. /s – 显示屏幕保护程序
  2. /p – 预览屏幕保护程序
  3. /c – 配置屏幕保护程序

此外,参数可以用冒号分隔,例如:/c:1234567/P:1234567。应用程序使用以下枚举作为输入参数:

public enum LaunchType
{
    [Description("No parameters")]
    Default = 0,

    [Description("\\s, Show the screen saver")]
    Show,

    [Description("\\c, Configure settings")]
    Configure,

    [Description("\\p, Show in preview mode")]
    Preview
}

输入参数被小写,用冒号分隔,并与预期的 string 进行比较。结果设置了两个变量:启动类型和窗口描述符,后者在预览模式下使用。

var launchType = LaunchType.Default;
var previewWindowDescriptor = 0;

logger.Log($"Start parameters: {string.Join("; ", e.Args)}", Category.Info);
if (e.Args.Length > 0)
{
    var firstArgument = e.Args[0].ToLower().Trim();
    string secondArgument = null;

    // Handle cases where arguments are separated by colon.
    // Examples: /c:1234567 or /P:1234567
    if (firstArgument.Length > 2)
    {
        secondArgument = firstArgument.Substring(3).Trim();
        firstArgument = firstArgument.Substring(0, 2);
    }
    else if (e.Args.Length > 1)
        secondArgument = e.Args[1];

    if (string.Equals("/c", firstArgument))
        launchType = LaunchType.Configure;
    else if (string.Equals("/s", firstArgument))
        launchType = LaunchType.Show;
    else if (string.Equals("/p", firstArgument))
        launchType = LaunchType.Preview;

    if (!string.IsNullOrEmpty(secondArgument))
        previewWindowDescriptor = Convert.ToInt32(secondArgument);
}
logger.Log($"Converted start parameters: launchType={launchType}, 
           previewWindowDescriptor={previewWindowDescriptor}");

然后,使用 switch 根据启动类型提供不同的行为。

显示屏幕保护程序

由于主窗口已创建,因此可以显示它。但是,如果计算机有多个显示器,则存在问题。默认情况下,操作系统会使除主显示器之外的所有其他显示器变黑,因此只需在主屏幕上显示主窗口即可。根据应用程序设置,用户可能希望在所有显示器上显示屏幕保护程序。WPF windows on two screens 这篇文章展示了如何将 WPF 窗口定位到辅助显示器或在两个显示器上显示两个窗口。

因此,应用程序在主显示器上显示主窗口,为所有辅助显示器创建并定位 EmptyWindowMainWindow 的新实例。然后将 Shutdown 模式设置为默认值。

Current.ShutdownMode = ShutdownMode.OnMainWindowClose;

配置屏幕保护程序

在此模式下,应用程序显示设置窗口。由于应用程序使用 Ikc5.Prism.Settings 包,因此它包含 SettingsWindow,允许用户设置应用程序的设置和选项。但主窗口不会显示,应该在不关闭应用程序的情况下关闭。另一方面,如果主窗口不关闭,应用程序将继续在后台执行。这就是为什么应用程序显示 SettingsWindow,将关闭模式设置为 ShutdownMode.OnLastWindowClose,然后关闭主窗口。当用户关闭设置窗口时,它被视为应用程序中的最后一个窗口,应用程序退出。

var settingsWindow = bootstrapper.Container.Resolve<SettingsWindow>();
settingsWindow.Show();

Current.ShutdownMode = ShutdownMode.OnLastWindowClose;
Current.MainWindow.Close();

预览屏幕保护程序

在此模式下,屏幕保护程序显示在一个小窗口中。此模式要求应用程序应适应屏幕的小尺寸,并且 WPF 窗口显示在 Win32 窗口中。另一个问题是,有必要捕获父窗口被释放时的事件,并关闭应用程序。否则,应用程序的当前实例将继续在后台执行。必要的代码已涵盖在上述关于 WPF 屏幕保护程序的文章中,因此这里有一些稍作修饰的代码。

var mainWindow = Current.MainWindow as MainWindow;
if (mainWindow == null)
{
    Current.Shutdown();
    return;
}

logger.Log("Init objects for preview mode");
var pPreviewHandle = new IntPtr(previewWindowDescriptor);
var lpRect = new RECT();
var bGetRect = Win32API.GetClientRect(pPreviewHandle, ref lpRect);

var sourceParams = new HwndSourceParameters("sourceParams")
{
    PositionX = 0,
    PositionY = 0,
    Width = lpRect.Right - lpRect.Left,
    Height = lpRect.Bottom - lpRect.Top,
    ParentWindow = pPreviewHandle,
    WindowStyle = (int)(WindowStyles.WS_VISIBLE | 
                        WindowStyles.WS_CHILD | WindowStyles.WS_CLIPCHILDREN)
};

logger.Log($"Source param size = ({0}, {0}, {lpRect.Right - lpRect.Left}, 
                                 {lpRect.Bottom - lpRect.Top})");
_winWpfContent = new HwndSource(sourceParams)
{
    RootVisual = mainWindow.MainGrid
};

// Event that triggers when parent window is disposed - used when doing
// screen saver preview, so that we know when to exit. If we didn't
// do this, Task Manager would get a new .scr instance every time
// we opened Screen Saver dialog or switched dropdown to this saver.
_winWpfContent.Disposed += (o, args) =>
{
    logger.Log("_winWpfContent is Disposed, close main window and application");
    mainWindow.Close();
    Current.Shutdown();
};
logger.Log(
    $"MainWindow is shown in preview, IsVisible={mainWindow.IsVisible}, 
              IsActive={mainWindow.IsActive}, Owner={mainWindow.Owner?.Title}" +
    $", Rect=({mainWindow.Left}, {mainWindow.Top}, 
              {mainWindow.Width}, {mainWindow.Height})");

安装屏幕保护程序

该解决方案包含 Publish 配置,该配置在构建后步骤中将可执行文件重命名为 Ikc5.ScreenSaver.scr

if $(ConfigurationName) NEQ Publish Exit 0

cd "$(TargetDir)"
del "$(TargetName).scr"
del "$(TargetName).scr.config"
ren "$(TargetFileName)" "$(TargetName).scr"
ren "$(TargetFileName).config" "$(TargetName).scr.config"

要安装屏幕保护程序,需要右键单击文件资源管理器中的 Ikc5 ScreenSaver.scr 文件,然后单击 Install 项。屏幕保护程序窗口允许设置超时、屏幕保护程序设置和预览。下面的图片展示了这些步骤。

历史

  • 2017 年 1 月 28 日 - 初次发布
  • 2017 年 1 月 29 日 - 更新了 zip 文件(包括来自 GIT 仓库的修复)
© . All rights reserved.