В этой лекции мы закрепим изученный ранее материал, создав с использованием XNA полнофункциональный хранитель экрана ( ), который отображает на экране фейерверк: вращающийся круг, из которого вылетает множество разноцветных искр (рисунок 4.1).
Что такое хранитель экрана? Это приложение, автоматически запускаемое Windows по истечению определенного срока бездействия компьютера. Изначально использование хранителя экрана было обусловлено сугубо практическими соображения: первые мониторы обладали весьма ограниченным ресурсом, в результате чего длительное отображение статичного изображения приводило к выгоранию люминофора в определенных местах и, соответственно, "запоминанию" монитором этого Norton Commander приводило к выжиганию в люминофоре монитора "изображения" данных панелей
(рис 4.1) Хранитель экранаТехнически хранитель экрана представляет собой исполняемый exe -файл с расширением .. Запуская хранитель экрана, Windows предает ему один из трех параметров командной строки, перечисленных в таблице 4.1. Разработчики C++ обычно создают хранители экрана с использованием библиотеки Scrnsave.lib (или Scrnsavw.lib ), которые самостоятельно обрабатывают параметры командной строки и реализуют завершение работы хранителя экрана при перемещении мыши, нажатии клавиши и т.п. Фактически разработчику необходимо лишь переопределить обработчик события WM_PAINT и реализовать код диалогового окна.
Примечание
К справедливости следует отметить, что реализация диалогового окна средствами Win32 API является далеко не самой приятной задачей.
| Параметр командной строки | Описание |
|---|---|
/s |
Запускает хранитель экрана в обычном режиме |
/c{:n} |
Показывает диалоговое окно конфигурации хранителя экрана. n – дескриптор родительского диалогового окна Display Properties |
/p {n} |
Запускает хранитель экрана в окне предварительного просмотра с дескриптором n. |
| Нет параметров | Показывает диалоговое окно конфигурации хранителя экрана |
В настоящее время .NET Framework 2.0 не содержит аналога библиотеки Scrnsave.lib, поэтому нам придется реализовывать всю функциональность самим. Это далеко не такая тривиальная задача, как хотелось бы, но и отнюдь не архисложная. Так что в путь!
Примечание
В состав Visual Studio 2005 Pro входит Screen Saver Starter Kit, позволяющий создать проект готового хранителя экрана буквально одним щелчком мыши. Но, к сожалению, Screen Saver Starter Kit имеет ряд
Учитывая сложность стоящей перед нами задачи, мы разобьем ее на несколько этапов. Начнем мы с визуализации вращающегося цветного диска. Создайте новый проект Windows Forms, установив в диалоговом окне New Project флажок Create directory for solution (в последствие мы добавим в решение проект инсталлятора). Нам придется выполнять визуализацию как в полноэкранном режиме, так и в окне предварительного просмотра, поэтому весь код визуализации будет логично разместить в отдельном классе. Добавьте в проект новый класс Firework (Project | Add Class).
Внутри класса Firework могут возникать критические исключения (например, при инициализации графического устройства). Обрабатывать в коде, использующем класс Firework, разношерстные исключения вроде ошибки создания графического устройства дольно утомительно, поэтому класс Firework будет перехватывать низкоуровневые исключения и генерировать свое собственное исключение FireworkException с ясным описанием причины возникновения ошибки.
class FireworkException : Exception
{
public FireworkException(string message) : base(message)
{
}
}
Для визуализации круга классу Firework необходимо загрузить и скомпилировать эффект. Только вот где его хранить? Типовой хранитель экрана обычно состоит из одного файла с расширением .fx -файла является далеко не самой лучшей идеей. К счастью Visual Studio позволяет легко внедрить fx -файл непосредственно в exe -файл. Для этого включите в проект файл ColorFill.fx, используемый во всех примерах этой лекции, и присвойте свойству Build Action этого файла значение Embedded Resource (рисунок 4.2).
(рис 4.2) Внедрение fx-файла в сборку Основные фрагменты кода класса Firework приведены в листинге 4.2. Полный текст примера находится в example.zip в каталоге Examples\Ch04\Ex01.
class Firework : IDisposable
{
// Место расположение ресурса с кодом файла эффекта
const string effectFileName = "GSP.XNA.Book.Ch04.Ex01.Data.ColorFill.fx";
// Количество сегментов в диске
const int slices = 64;
// Угловая скорость вращения диска (радиан в секунду)
public const float diskSpeed = 3.0f;
// Радиус диска
public const float diskRadius = 0.015f;
// Текущий угол поворота float diskAngle = 0;
// Дескриптор окна, на которое будет осуществляться визуализация
...
// Конструктор. Принимает:
// hWnd – дескриптор окна в котором осуществляется визуализация.
// scintillaSize – размер искр (пока игнорируется)
// scintillaInterval – интервал между искрами (пока игнорируется)
public Firework(IntPtr hWnd, float scintillaSize,
float scintillaInterval)
{
// Сохраняем дескриптор окна this.hWnd = hWnd;
presentParams = new PresentationParameters();
presentParams.BackBufferCount = 1;
presentParams.SwapEffect = SwapEffect.Discard;
presentParams.PresentationInterval = PresentInterval.One;
try {
GraphicsDeviceCapabilities caps = GraphicsAdapter.
DefaultAdapter.GetCapabilities(DeviceType.Hardware);
CreateOptions options = CreateOptions.SingleThreaded; if
(caps.DeviceCapabilities.SupportsHardwareTransformAndLight)
options |= CreateOptions.HardwareVertexProcessing; else
options |= CreateOptions.SoftwareVertexProcessing;
device = new GraphicsDevice(GraphicsAdapter.DefaultAdapter,
DeviceType.Hardware,
hWnd, options, presentParams);
}
// Это исключение обычно генерируется при отключенном аппаратном
ускорении в Display
// Properties
catch (DeviceNotSupportedException)
{
// Перехватываем исключение и генерируем
собственное исключение с более понятным описанием
// проблемы
throw new FireworkException("Не могу создать устройство Direct3D");
}
decl = new VertexDeclaration(device, VertexPositionColor.VertexElements);
diskVertices = new VertexPositionColor[slices + 2];
// Загружаем эффект из ресурсов сборки
Stream effectStream = Assembly.GetExecutingAssembly().
GetManifestResourceStream(
4> effectFileName) ;
// Выполняем компиляцию эффекта
CompiledEffect compiledEffect = Effect.CompileEffectFromFile
(effectStream, null,
null, CompilerOptions.None, TargetPlatform.Windows);
if (!compiledEffect.Success)
throw new FireworkException(String.Format(
"Ошибка при компиляции эффекта: \r\n{0}", compiledEffect.
ErrorsAndWarnings));
effect = new Effect(device, compiledEffect.GetEffectCode(),
CompilerOptions.NotCloneable, null);
if (!effect.CurrentTechnique.Validate())
throw new FireworkException(String.Format
("Ошибка при валидации техники \"{0}\""+
"эффекта \"{1}\"\n\rСкорее всего,
функциональность шейдера превышает возможности GPU",
effect.CurrentTechnique.Name, effectFileName));
stopwatch = new Stopwatch();
stopwatch.Start();
}
// Рассчитывает угол, на который должен повернуться диск с
момента последнего вызова этого
// метола и выполняет собственно поворот.
public void Update()
{
// Так как хранитель экрана может работать часами, значение
переменной currentTime может
// достигнуть достаточно большой величины. Поэтому, во избежание
падения точности вычислений
// используется тип double
double currentTime = (float)stopwatch.ElapsedTicks /
(float)Stopwatch.Frequency;
// Переменная delta принимает очень ограниченный диапазон
значений, поэтому здесь вполне
// можно обойтись типом float
float delta = (float)(currentTime - lastTime);
// Корректируем угол поворота диска
diskAngle += diskSpeed * delta;
// Рассчитываем новые координаты вершин диска
diskVertices[0] = new VertexPositionColor(new Vector3(0.0f, 0.0f, 0.0f),
XnaGraphics.Color.LightGray);
for (int i = 0; i <= slices; i++) {
float angle = (float)i / (float)slices * 2.0f * (float)Math.PI;
float x = diskRadius * (float)Math.Sin(diskAngle + angle);
float y = diskRadius * (float)Math.Cos(diskAngle + angle);
byte red = (byte)(255 * Math.Abs(Math.Sin(angle * 3)));
byte green = (byte)(255 * Math.Abs(Math.Cos(angle * 2)));
diskVertices[i + 1] = new VertexPositionColor(new Vector3(x, y, 0.0f),
new
XnaGraphics.Color(red, green, 128));
};
lastTime = currentTime;
}
// Выполняет визуализацию изображения
public void Paint()
{
}
// Освобождает ресурсы формы
public void Dispose()
{
if (stopwatch != null)
stopwatch.Stop() ;
if (device != null)
{
device.Dispose() ; device = null;
}
}
}
Теперь, давайте, интегрируем функциональность класса Firework в главную форму приложения (листинг 4.3).
public partial class FullscreenForm : Form
{
Firework firework = null;
public FullscreenForm()
{
InitializeComponent();
}
private void FullscreenFormLoad(object sender, EventArgs e)
{
SetStyle(ControlStyles.Opaque | ControlStyles.ResizeRedraw, true);
MinimumSize = SizeFromClientSize(new Size(1, 1));
try
{
firework = new Firework(Handle, 0.0f, 0.0f);
}
catch (FireworkException ex)
{
MessageBox.Show(ex.Message, "Критическая ошибка", MessageBoxButtons.OK,
MessageBoxIcon.Error);
}
Application.Idle += new EventHandler(ApplicationIdle);
}
void Application_Idle(object sender, EventArgs e)
{
if (firework == null)
{
Close () ; return;
}
firework.Update();
Invalidate() ;
}
private void FullscreenFormFormClosed(object sender, FormClosedEventArgs e)
{
if (firework != null)
{
firework.Dispose(); firework =
null;
}
}
private void FullscreenFormPaint(object sender, PaintEventArgs e)
{
if (firework == null)
return;
firework.Paint();
}
}
В заключение необходимо развернуть форму на весь экран и убрать заголовок. Для этого присвойте свойству FormBorderStyle значение None, а свойству значение Maximized. После запуска полученного приложения экран окрасится в черный цвет, а в центре появится маленький вращающийся пестрый диск.
На следующем этапе мы добавим в наше приложение снопы искр, вылетающие из крутящегося диска. Траектория движения искры, напоминающая спираль, будет складываться как композиция движения искры из центра диска по прямой с постепенным замедлением и движения по окружности вокруг диска с постоянно уменьшающейся угловой скоростью.
Начальный цвет искры будет совпадать с цветом диска в окрестностях появившейся искры. С течением времени искра постепенно затухает посредством уменьшения коэффициента непрозрачности, пока он не достигнет нуля, после чего искра считается потухшей. Всю эту функциональность разумно инкапсулировать в отдельном классе, вложенном в класс Firework (листинг 4.4).
class Scintilla
{
// Время жизни частицы
const float StartTime = 6.0f;
// Диапазон скоростей прямолинейного движения частицы
const float MinSpeed = 0.3f;
const float MaxSpeed = 0.45f;
// Коэффициент замедления прямолинейного движения частицы
const float tSlowing = 0.105f;
// Коэффициент замедления угловой скорости вращательного движения частицы
const float rSlowing = 0.25f;
// Цвет частицы
public Color color;
// Текущие угол поворота частицы вокруг круга и расстояние от центра круга (т.е. координаты
// частицы задаются в сферической системе координат)
public float angle;
public float distance;
// Оставшееся время жизни частицы
public float time;
// Текущая скорость прямолинейного движения
public float tSpeed;
// Текущая скорость вращательного движения
public float rSpeed;
// Возвращает структуру с информацией о частице
public VertexPositionColor Vertex
{
get
{
VertexPositionColor vertex;
// Рассчитываем декартовые координаты частицы
vertex.Position.X = distance * (float)Math.Sin(angle);
vertex.Position.Y = distance * (float)Math.Cos(angle);
vertex.Position.Z = 0;
// Вычисляем новый коэффициент непрозрачности частицы
vertex.Color = new Color(color.R, color.G, color.B,
(byte)(255f * time /
StartTime));
return vertex;
}
}
// Рассчитывает параметры новой частицы. diskAngle – угол поворота диска, используемый для
// синхронизации цвета частицы с цветом диска в окрестностях вершины public void
Init(float diskAngle)
{
// Вычисляем начальное расстояние вершины от центра диска (вершина должна находиться в центре
// диска). Генератор случайных чисел rnd объявлен в классе
Firework
distance = (float)rnd.NextDouble() * Firework.diskRadius;
// Вычисляем начальный угол поворота искры вокруг диска
angle = (float)Firework.rnd.NextDouble() * 2.0f * (float)Math.PI;
// Определяем начальную скорость прямолинейного движения искры
tSpeed = MinSpeed + (float)Firework.rnd.NextDouble() *
(MaxSpeed - MinSpeed);
// Угловая скорость движения вершины всегда в 4 раза меньше
скорости диска rSpeed = Firework.diskSpeed / 4. 0f;
// Рассчитываем начальный цвет искры (он должен совпадать с цветом диска)
byte red = (byte)(255 * Math.Abs(Math.Sin((angle - diskAngle) * 3)));
byte
green = (byte)(255 * Math.Abs(Math.Cos((angle - diskAngle) * 2)));
color =
new Color(red, green, 128, 255);
// Задаем оставшееся время жизни искры time
= StartTime;
}
// Обновляет состояние искры. delta - время, прошедшее с момента
последнего вызова метода
// Update
public void Update(float delta)
{
// Если искра еще не потухла if (time gt;
0.0f)
{
// Уменьшаем оставшееся время жизни искры
time = time - delta;
// Корректируем скорости прямолинейного и вращательного движений
tSpeed = Math.Max(tSpeed - tSlowing * delta, 0.0f); rSpeed =
Math.Max(rSpeed - rSlowing * delta, 0.0f);
// Корректируем положение искры в пространстве distance += tSpeed *
delta;
angle += rSpeed * delta;
}
}
}
Пользователь сможет управлять видом фейерверка искр посредством двух параметров:
Так как количество искр на экране постоянно меняется, встает вопрос хранения информации об искрах. В принципе, для этой цели вполне подходит обобщенный класс List за исключением пары нюансов:
DrawUserPrimitives может визуализировать вершины исключительно из массивов. Однако преобразование списка List в массив посредством метода ToArray() сопряжено с выделением памяти, что в свою очередь может привести к частым вызовам сборщика мусора и, соответственно, провалам производительности.Первый недостаток мы обойдем достаточно хитро. Потухшие искры не будут удаляться из списка - вместо этого информация о новых искрах будет просто заноситься в элементы списка с потухшими искрами, затирая их, и лишь при отсутствии таковых добавляться в конец списка. Для борьбы со вторым недостатком мы будем самостоятельно копировать содержимое списка в массив, при этом выделение памяти для массива будет производиться лишь при недостаточном размере целевого массива. В листинге 4.5 приведены фрагменты обновленного класса Firework с учетом вышеприведенных требований.
class Firework : IDisposable
{
// Дискретный шаг времени, с которым выполняется моделирование логики работы хранителя экрана
const float timeStep = 0.005f;
// Максимальное время между вызовами Update не должно превышать 1 секунды. Большие временные
// интервалы игнорируются
const float maxDelta = 1.0f;
// Максимальное количество искр, которое может появиться за один дискретный шаг времени (5
// миллисекунд)
int maxScintillaCount;
// Вероятность появления следующей искры
const float scintillaProbability = 0.3f;
// Размер искры
float scintillaSize;
...
// Время, прошедшее с момента запуска приложения
double lastTime = 0;
// Список искр
List<Scintilla> scintillas;
// Массив вершин для визуализации искр
VertexPositionColor[] scintillasVertices = null;
// Количество вершин, хранящихся в массиве
int scintillasVertexNount = 0;
// Генератор случайных чисел, используемый классами Firework и Scintilla
public static Random rnd = new Random();
// Конструктор
public Firework(IntPtr hWnd, float scintillaSize, float scintillaInterval)
{
this.hWnd = hWnd;
// Запоминаем пользовательские настройки
this.scintillaSize = scintillaSize;
this.maxScintillaCount = maxScintillaCount;
...
// Создаем список вершин
scintillas = new List<Scintilla>(16);
// Создаем массив вершин
scintillasVertices = new VertexPositionColor[16];
...
}
// Обновляет сцену
public void Update()
{
double currentTime = (float)stopwatch.ElapsedTicks / (float)Stopwatch.Frequency;
// Максимальный временной интервал не должен превышать maxDelta if (currentTime –
lastTime > maxDelta) lastTime = currentTime - maxDelta;
// Количество дискретных шагов timeStep, которые необходимо выполнить
int stepCount = (int)Math.Floor((currentTime - lastTime) / timeStep);
// Интервал между двумя вызовами метода Update, с учетом дискретности времени float
delta = stepCount * timeStep;
// Поворачиваем диск
diskAngle += diskSpeed * delta;
// Корректируем положение вершин диска
...
// Моделируем движение искр с дискретным шагом времени for (int i =
0; i < stepCount; i++)
{
lastTime += timeStep;
// Счетчик количества не потухших искр scintillasVertexСount = 0;
// Количество новых искр, которые могут появиться на данном шаге
int scintillaCount = rnd.Next(maxScintillaCount + 1);
// Перебираем все искры
for (int j = 0; j < scintillas.Count; j++)
{
// Обновляем состояние текущей искры
scintillas[j].Update(delta);
// Если искра не потухла
if (scintillas[j].time gt; 0)
{
// Увеличиваем счетчик не потухших искр
scintillasVertexСount++;
}
else
// Если искра является потухшей
{
// Пока не исчерпан лимит новых искр
while (scintillaCount gt; 0)
{
// Пробуем добавить новую искру, поэтому уменьшаем счетчик искр
scintillaCount--;
// Генерируем новую искру с вероятностью scintillaProbability
if ((Firework.rnd.NextDouble() lt; scintillaProbability))
{
// Инициализируем текущую искру
scintillas[j].Init(diskAngle);
// Увеличиваем счетчик искр
scintillasVertexСount++; break;
}
}
}
}
// Если необходимо создать еще несколько новых искр while
(scintillaCount gt; 0)
{
scintillaCount--;
if ((Firework.rnd.NextDouble() lt; scintillaProbability))
{
// Добавляем в список информацию о новой искре
scintillas.Add(new Scintilla());
scintillas[scintillas.Count - 1].Init(diskAngle);
scintillasVertexNount++;
}
}
}
// Если число искр превышает размер массива вершин
if (scintillasVertexNount > scintillasVertices.Length)
{
// Удваиваем размер массива. Если размер удвоенного массива недостаточен, используем в
// качестве размера массива текущее количество вершин (на всякий случай перестраховываемся)
scintillasVertices = new VertexPositionColor[Math.Max(scintillasVertexNount,
scintillasVertices.Length * 2)];
}
// Копируем информацию о искрах в массив вершин int
k = 0;
for (int i = 0; i < scintillas.Count; i++)
// Учитывает только не потухшие искры if
(scintillas[i].time > 0)
{
scintillasVertices[k] = scintillas[i].Vertex; k++;
}
}
// Визуализация сцены public
void Paint()
{
device.RenderState.CullMode = CullMode.None;
// Задаем режим смешения пикселей для моделирования полупрозрачности
device.RenderState.AlphaBlendEnable = true;
device.RenderState.BlendFunction = BlendFunction.Add;
device.RenderState.SourceBlend = Blend.SourceAlpha;
device.RenderState.DestinationBlend = Blend.InverseSourceAlpha;
// Задаем размер точек (искр)
device.RenderState.PointSize = scintillaSize;
// Визуализируем массив искр
if (scintillasVertexNount > 0)
{
device.DrawUserPrimitives(PrimitiveType.PointList, scintillasVertices, 0,
scintillasVertexNount);
}
// Визуализируем вращающийся диск
device.DrawUserPrimitives(PrimitiveType.TriangleFan, diskVertices, 0,
4> diskVertices.Length - 2);
}
Обработчик события Load формы также нуждается в косметической правке:
private void FullscreenFormLoad(object sender, EventArgs e)
{
// Задаем размеры искр и максимальное количество искр, генерируемое каждые 5 миллисекунд
firework = new Firework(Handle, 3.0f, 10f);
}
Ну что ж, настало время подумать о преобразовании приложения в хранитель экрана. Как известно, любой хранитель экран должен автоматически завершать работу приложения при активности пользователя. Начнем с клавиатуры - нажатие любой клавиши клавиатуры должно немедленно завершать работу приложения (листинг 4.6).
private void FullscreenFormKeyDown(object sender, KeyEventArgs e)
{
Close () ;
}
Аналогичным образом приложение должно завершать работу при нажатии кнопки мыши или перемещении курсора мыши. Но здесь есть одна тонкость. Дело в том, что курсор мыши с высокой чувствительностью может реагировать даже на незначительные воздействия вроде микроколебаний стола из-за проезжающего за окном поезда. Поэтому во избежание непреднамеренных прерываний хранителя экрана мы будет завершать работу приложения только после того, как курсор отодвинется от первоначального положения в момент активации хранителя экрана на расстояние порядка 10 пикселей (листинг 4.7).
public partial class FullscreenForm : Form
{
// Флаг, устанавливаемый в true после первого вызова обработчика события MouseMove
bool isMouseActive = false;
// Координаты мыши при первом вызове обработчика события MouseMove
System.Drawing.Point mouseLocation;
...
// Обработчик события MouseDown, завершающий работу приложения при нажатии кнопки мыши
private void FullscreenForm_MouseDown(object sender, MouseEventArgs e)
{
Close();
}
// Обработчик события MouseMove
private void FullscreenForm_MouseMove(object sender, MouseEventArgs e)
{
// Обработчик события MouseMove запускается впервые if (!isMouseActive)
{
isMouseActive = true;
// Запоминаем текущие
координаты мыши
mouseLocation = e.Location;
}
else
{
// Если курсор мыши переместился вдоль оси X или Y от своего первоначального положения
// больше, чем на 10 единиц
if ((Math.Abs(e.Location.X - mouseLocation.X) > 10) ||
(Math.Abs(e.Location.Y - mouseLocation.Y) > 10))
{
// Завершаем работу приложения Close();
}
}
}
}
В процессе работы хранителя экрана некоторое приложение может вывести на экран диалоговое окно с важной информацией (например, Internet Explorer по окончанию загрузки файла). При этом окно хранителя экран теряет фокус, который переходит к новому диалоговому окну. Хранитель экрана, поверх которого отображается диалоговое окно, будет выглядеть, мягко говоря, несколько странно, поэтому в качестве одного из критериев завершения работы хранителя экрана логично использовать потерю фокуса формой (листинг 4.8).
// Обработчик события Deactivate полноэкранной формы хранителя экран, завершающий работу
// приложения при потере формой фокуса
private void FullscreenFormDeactivate(object sender, EventArgs e)
{
Close () ;
}
Курсор мыши является чужеродным элементом для хранителя экрана, поэтому его необходимо скрыть посредством метода Hide класса Cursor:
private void FullscreenFormLoad(object sender, EventArgs e)
{
Cursor.Hide();
}
Следующее отличие хранителя экрана от обычного приложения состоит в том, что он должен активироваться только при запуске приложения с ключом /s. Соответственно, мы должны добавить в метод Main статического класса Program анализ параметров командной строки (листинг 4.10).
Примечание
Чтобы Visual Studio всегда запускала приложение с ключом /s, укажите этот параметр в поле Command line arguments вкладки Debug свойств проекта.
static class Program
{
[STAThread]
static void Main()
{
// Получаем массив параметров командной
строки
string[] args = Environment.GetCommandLineArgs();
// Если первый параметр равен "/S"
if ((args.Length == 2) (args[1].ToUpper() == "/S"))
{
// Отображаем форму приложения с хранителем экрана
Application.Run(new FullscreenForm()); return;
}
// Если параметр не является "/S", нечего не делаем
return;
}
}
В заключении необходимо присвоить файлу хранителя экрана расширение .. Переименовывать файл вручную после каждой компиляции приложения довольно утомительно, поэтому мы автоматизируем этот процесс. Откройте в свойствах проекта вкладку Build Events и введите в поле Post-build event command line следующую команду (рисунок 4.3): copy "$(TargetFileName)" "*..
Теперь после каждой компиляции приложения будет вызываться команда copy, создающая копию exe-файла приложения с расширением .. Обратите внимание на получение имени exe-файла приложения посредством встроенного макроса $(TargetFileName) , благодаря чему команда copy не привязана к фиксированному exe -файлу.
(рис 4.3) Вкладка Build Events Для проверки работоспособности хранителя экрана откройте каталог с . -файлом в файловом менеджере и вызовите его контекстное меню (рисунок 4.4). Как видно, контекстное меню любого исполняемого файла хранителя экрана содержит три пункта:
Test - запускает хранитель экрана на выполнение с ключом /s.Configure (Настроить) - открывает окно конфигурации хранителя экрана.Install (Установить) - открывает вкладку Screen Saver диалогового окна Display Properties и выбирает данный хранитель экрана в качестве текущего.Немного проигравшись с нашим хранителем экрана, вы заметите ряд недоделок. Например, при попытке открыть окно конфигурации ровным счетом нечего не происходит, а в окне предварительного просмотра (маленький "дисплейчик") диалогового окна Display Properties просто выводится изображение по умолчанию. А на компьютере с несколькими мониторами выяснится, что наш хранитель экрана активируется только основном мониторе. Что ж, работы нам предстоит еще много.
(рис 4.4) Контекстное меню исполняемого файла хранителя экрана
В настоящее время поддержка видеокартами двух мониторов уже стала нормой, поэтому любой уважающий себя разработчик должен позаботиться о корректном функционировании приложения на компьютере с несколькими мониторами. В частности, хранитель экрана должен показывать заставку на всех мониторах. Наиболее простое решение - просто отображать на всех мониторах одно и то же изображение. Так как наш хранитель экрана представляет собой форму, развернутую на весь экран, в случае нескольких мониторов мы можем просто создать несколько экземпляров формы - по одному на каждый монитор.
Начнем с метода Main. Информация об экранных координатах всех мониторов системы храниться в коллекции AllScreens класса Screen. Соответственно приложение должно просто перебрать элементы этой коллекции и использовать полученную информацию при создании форм (листинг 4.11).
static void Main()
{
string[] args = Environment.GetCommandLineArgs();
if ((args.Length == 2) (args[1].ToUpper() == "/S"))
{
// Перебираем все мониторы
foreach (Screen screen in Screen.AllScreens)
{
// Создаем форму размеров во весь монитор
FullscreenForm form = new FullscreenForm(screen);
// Отображаем форму
form. Show () ;
}
// Запускаем цикл обработки сообщений. Изображение форм будет обновляться посредством
// обработчиков события Idle, регистрируемых конструктором формы. Application.Run();
return;
}
return;
}
Конструктор формы, разумеется, так же придется подправить, ведь теперь он будет принимать информацию об экране, на котором будет отображаться форма. Вступать же в силу данный параметр будет после конструирования формы в обработчике события Load (листинг 4.12).
Screen screen = null;
public FullscreenForm(Screen screen)
{
this.screen = screen;
InitializeComponent();
}
private void FullscreenFormLoad(object sender, EventArgs e)
{
// Форма должна занимать весь экран
// Внимание! Свойство Bounds не оказывает влияния, если форма развернута на весь экран
// (т.е. когда свойство WindowsState равно Maximized) Bounds = screen.Bounds;
}
Наконец необходимо определиться с завершением работы. До сих пор все наши приложения содержали лишь одно главное окно, закрытие которого методом Close приводило к завершению работы всего приложения. Теперь же окон несколько, поэтому вызов метода Close закроет лишь единственное окно. Поэтому мы будет завершать работу приложения путем вызова метода Application.Exit. Правда у этого подхода есть один подводный камень - при завершении работы методом Application.Exit не вызываются обработчики события FormClosed. Поэтому код из обработчиков необходимо перенести в обработчики события FormClosing, корректно вызываемых методом Application.Exit. Другой нюанс связан с обработчиком события Deactivate: так мы создаем несколько форм, в процессе создания они будут неминуемо получать-терять фокус (ведь в каждый момент времени только одна форма может иметь фокус). Поэтому во избежание досрочного завершения хранителя экра
на в процессе инициализации приложения необходимо игнорировать событие Deactivate. Основные фрагменты обновленных обработчиков событий формы приведены в листинге 4.13.
// Ресурсы теперь освобождаются в обработчике события FormClosing
private void FullscreenFormFormClosing(object sender, FormClosingEventArgs e)
{
if (firework != null)
{
firework.Dispose() ; firework =
null;
}
}
private void FullscreenFormDeactivate(object sender, EventArgs e)
{
// Пока в приложение не запущен цикл обработки сообщений, игнорируем событие Deactivate if
(Application.MessageLoop) Application.Exit();
}
private void FullscreenForm_MouseDown(object sender, MouseEventArgs e)
{
// Обратите внимание на завершение приложения посредством метода Application.Exit (вместо
// Form.Close)
Application.Exit();
}
...
Готовое приложение можно найти в example.zip в каталоге Examples\Ch04\Ex04.
Настало время подумать об управлении пользовательскими настройками хранителя экрана. Работа нашего хранителя экрана управляется двумя параметрами: размером искр и максимальным числом искр, вылетающих в течение кванта времени, равного 5 миллисекунд. Второй параметр не является интуитивно понятным, ведь рядовому пользователю намного проще регулировать количество искр посредством ползунка "мало – много". Поэтому мы пойдем на небольшую хитрость: количество искр будет задаваться целочисленным параметром "плотность искр", лежащим в диапазоне от 0 (минимальное количество искр) до 9 (максимальное количество искр), а число искр, появляющихся каждые 5 секунд, будет рассчитываться уже на основе данного целочисленного параметра.
Итак, откройте вкладку настроек приложения (Properties | Setting) и добавьте в него два целочисленных параметра (рисунок 4.5):
(рис 4.5) Вкладка настроек приложения scintillaSize - размер искр. Значение по умолчанию 2scintillaDensity - плотность искр. Значение по умолчанию 5.Для начала немного подкорректируйте обработчик события Load полноэкранной формы хранителя экрана, чтобы он брал настройки непосредственно из файла конфигурации приложения (листинг 4.14).
public partial class FullscreenForm : Form
{
Properties.Settings settings;
private void FullscreenForm_Load(object sender, EventArgs e)
{ ...
// Загружаем настройки приложения из файла конфигурации (или настройки по умолчанию при
// отсутствии файла)
settings = new Properties.Settings();
try
{
// Количество искр, генерируемых каждые 5 мс, рассчитывается методом "научного
тыка" firework = new Firework(Handle, (float)settings.scintillaSize,
(settings.scintillaDensity + 1) * 5);
}
...
}
Теперь создайте новую форму и поместите на нее компоненты согласно рисунку 4.6 и таблице 4.2.
(рис 4.6) Диалоговое окно параметры.| Класс | Свойство | Значение |
|---|---|---|
SettingsForm (диалоговое окно) |
Name |
SettingsForm |
Text |
Параметры | |
ShowInTaskbar |
false |
|
TopMost |
true |
|
MinimizeBox |
false |
|
MaximizeBox |
false |
|
GroupBox |
Text |
Искры |
Label |
Text |
Размер |
TrackBar |
Name |
scintillaSizeTrackBar |
Minimum |
1 | |
Maximum |
4 | |
Label |
Text |
Плотность |
TrackBar |
Name |
scintillaDensityTrackBar |
Minimum |
0 | |
Maximum |
9 | |
Button |
Name |
okButton |
Text |
Ok |
|
Button |
Name |
cancelButton |
Text |
Отмена |
Для автоматической инициализации элементов управления диалогового окна ползунки scintillaSizeTrackBar и scintillaDensityTrackBar необходимо связать со свойствами scintillaSize и scintillaDensity из конфигурационного файла приложения. Это операция легко выполняется посредством свойства Application Settings | Value ползунков (рисунок 4.7).
(рис 4.7) Привязка значения ползунка scintillaSizeTrackBar к свойству scintillaSize файла конфигурации Следующий шаг - оживление формы посредством реализации нехитрых обработчиков сообщений (листинг 4.15).
public partial class SettingsForm : Form
{
public SettingsForm()
{
InitializeComponent();
}
// Обработчик нажатия кнопки Ok
private void okButton_Click(object sender, EventArgs e)
{
Properties.Settings settings = new Properties.Settings();
// Задаем новые значения свойств файла конфигурации
settings.scintillaSize = scintillaSizeTrackBar.Value;
settings.scintillaDensity = scintillaDensityTrackBar.Value;
// Сохраняем информацию в файле
settings.Save();
// Закрываем форму и завершаем приложение
Close ();
}
// Обработчик нажатия кнопки Отмена
private void cancelButtonClick(object sender, EventArgs e)
{
// Просто завершаем приложение, не сохраняя изменения
Close ();
}
}
Как говорилось в начале лекции, диалоговое окно настроек хранителя экрана должно отображаться в 3-х случаях:
/C./C:n, где n - дескриптор диалогового окна Display Properties. Например, /C:299792.Для начала, мы можем попробовать добавить в функцию Main статического класса Program следующий код:
static void Main()
{
// Если приложение запущено без параметров или первый параметр начинается на "/C"
if ((args.Length == 1) || ((args.Length == 2)
(args [1] .Length >= 2) (args [1] .ToUpper () .Substring(0, 3) == "/C")))
{
// Отображаем диалоговое окно
Application.Run(new SettingsForm());
return;
}
}
В принципе, подобный подход является вполне работоспособным, если не считать одной особенности: диалоговое окно может появиться на совершенно другом конце экрана (и даже на другом мониторе) относительно окна Display Properties. Для хранителя экрана, сделанного на профессиональном уровне, такое поведение не допустимо, поэтому нам необходимо решить данную проблему.
(рис 4.8) Начальное положение диалоговое окна Параметры не связано с текущим положением окна Display Properties Для выравнивания диалогового окна по центру окна Display Properties необходимо определить положение этого самого окна Display Properties на экране. Здесь самое время вспомнить о том, что при запуске хранителя экрана диалоговое окно Display Properties передает ему в качестве параметра свой дескриптор. Ну а немного поколдовав с Win32 API над дескриптором окна, можно легко получить об этом окне практически любую информацию.
Для начала мы добавим в класс диалогового окна еще один конструктор, принимающий в качестве параметра дескриптор окна Display Properties (листинг 4.17).
public partial class SettingsForm : Form
{
// Необходимо ли центрировать окно параметров относительно окна Display Properties. По
// умолчанию центрирование не выполняется
bool center = false;
// Дескриптор окна Display Properties
IntPtr parentHandle;
public SettingsForm(IntPtr parentHandle):this()
{
// Запоминаем дескриптор
this.parentHandle
= parentHandle;
// Включаем центрирование
center = true;
}
}
Чтобы задействовать новый конструктор в коде функции Main необходимо реализовать более детальный разбор параметров с выделением из ключа вида /C:n значения дескриптора (листинг 4.18).
static void Main()
{
// Если приложение запущено без параметров (такое происходит, к примеру, при открытии окна
// конфигурации при помощи контекстного меню) или с ключом "/C"
if ((args.Length == 1) || ((args.Length >= 2) args[1].ToUpper() == "/C"))
{
// Открываем диалоговое окно параметров приложения без центрирования
Application.Run(new SettingsForm()); return;
}
// Если параметр хранителя экрана имеет вид /C:n
if ((args.Length == 2) (args[1].Length > 3)
(args[1].ToUpper().Substring(0, 3) == "/C:"))
{
// Выделяем из строки дескриптор окна и преобразуем его в значение типа IntPtr
IntPtr hWnd = (IntPtr)int.Parse(args[1].ToUpper().Substring(3, args[1].Length - 3));
// Открываем диалоговое окно с выравниванием по центру окна Display Properties с дескриптором
// hWnd
Application.Run(new SettingsForm(hWnd));
}
}
Переходим к самому интересному - получению информации о положении окна по его дескриптору. Немного порывшись в MSDN мы обнаружим требуемую нам функцию:
BOOL GetWindowRect(HWND hWnd, LPRECT lpRect);
где
hWnd - дескриптор окна;lpRect - указатель на структуру rect, в которую заносятся координаты верхнего левого и нижнего правого углов окна.К сожалению . Так как методы Win32 API наверняка еще не один раз пригодиться нам, будет разумно вынести их в отдельный статический класс Win32 (листинг 4.19). Дополнительным плюсом подобного подхода является простая идентификация в тексте программы вызовов методов Win32, являющихся потенциальными источниками проблем при переносе приложения на другие платформы (например, на x64 или Xbox 360 ).
// Определения функций Win32 API, написанные на основе
материалов сайта http://pinvoke.net
using System.Runtime.InteropServices;
public static class Win32
{
// Структура RECT
[StructLayout(LayoutKind.Sequential)]
public struct RECT
{
public int left; public
int top; public int
right; public int
bottom;
// Добавляем "от себя" два свойства, позволяющие
легко определить ширину и высоту
области public int Width { get { return right - left + 1;
}
}
public int Height { get
{
return bottom - top + 1;
}
}
}
// Возвращает координаты окна
[DllImport("user32.dll")]
public static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
}
И, наконец, последний штрих - реализация в обработчике Load диалогового окна выравнивания по центру окна Display Properties (листинг 4.20).
private void SettingsForm_Load(object sender, EventArgs e)
{
if (center)
{
// Создаем структуру RECT
Win32.RECT rect = new Win32.RECT();
// Получаем информацию о местоположении окна Display Properties
Win32.GetWindowRect(parentHandle, out rect);
// Вычисляем координаты центра окна Display Properties int
centerX = (rect.right + rect.left) / 2; int centerY
= (rect.bottom + rect.top) / 2;
// Позиционируем наше диалоговое окно по центру окна Display Properties
Left = centerX - Width / 2;
Top = centerY - Height / 2;
}
}
Готовое приложение можно найти в example.zip в каталоге Examples\Ch04\Ex05.
Переходим к заключительному и самому нетривиальному этапу – отображению хранителя экрана в окне предварительного просмотра. Чтобы реализовать эту функциональность приложение должно получить из командной строки дескриптор области предварительного просмотра (мониторчик на вкладке Screen Saver окна Display Properties ) и создать в этом окне свой элемент управления. Для анимации созданного элемента управления окно Display Properties будет автоматически посылать ему сообщения WM_PAINT, а при выборе другого хранителя экрана, смене вкладки или закрытии диалогового окна Display Properties – сообщение WM_CLOSE.
К сожалению, большую часть этой функциональности не возможно реализовать средствами .NET Framework поэтому на придется опуститься до уровня оконных процедур и циклов обработки сообщений Win32. Для начала определим все необходимые константы, структуры и функции Win32 (листинг 4.21).
public static class Win32
{
...
// Определение делегата оконной функции обработки сообщений.
public delegate IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);
// Структура WNDCLASSEX, используемая при регистрации класса окна
[StructLayout(LayoutKind.Sequential)] public struct WNDCLASSEX
{
[MarshalAs(UnmanagedType.U4)]
public int cbSize;
[MarshalAs(UnmanagedType.U4)]
public uint style;
public WndProc lpfnWndProc;
public int cbClsExtra;
public int cbWndExtra;
public IntPtr hInstance;
public IntPtr hIcon;
public IntPtr hCursor;
public IntPtr hbrBackground;
public string lpszMenuName;
public string lpszClassName;
public IntPtr hIconSm; }
// Битовые флаги стилей окна [Flags]
public enum WindowStyles : uint
{
// Окно является дочерним
WS_CHILD = 0x40000000,
// Окно сразу же является видим
WS_VISIBLE = 0x10000000,
// Окно игнорирует действия пользователя
WS_DISABLED = 0x08000000,
...
}
// Битовые флаги стилей класса окна
[Flags]
public enum ClassStyles : uint
{
// Окно будет использовать контекст устройства родительского окна
CS_PARENTDC = 0x0080,
...
}
// Идентификаторы сообщений Windows
public enum WindowsMessages : uint
{
WM_CLOSE = 0x10, WM_DESTROY = 0x2,
WM_PAINT = 0xF,
...
}
// Регистрирует класс окна [DllImport("user32")] public static extern short
RegisterClassEx([In] ref WNDCLASSEX lpwcx);
// Создает новое окно
[DllImport("user32.dll")]
public static extern IntPtr CreateWindowEx(uint dwExStyle, string lpClassName,
4> string lpWindowName, uint dwStyle, int x, int y, int nWidth, int nHeight, 4> IntPtr
hWndParent, IntPtr hMenu, IntPtr hInstance, IntPtr lpParam);
// Возвращает координаты клиентской области окна [DllImport("user32.dll")]
public static extern bool GetClientRect(IntPtr hWnd, out RECT lpRect);
// Уничтожает окно
[DllImport("user32.dll")] public static extern bool
DestroyWindow(IntPtr hWnd);
// Помещает в очередь сообщений WMQUIT, завершающее выполнение цикла обработки сообщений
[DllImport("user32.dll")] public static extern void PostQuitMessage(int nExitCode);
// Вызывает обработчик сообщения по умолчанию
[DllImport("user32.dll")]
public static extern IntPtr DefWindowProc(IntPtr hWnd, uint uMsg, IntPtr wParam, IntPtr
lParam);
}
В функцию Main необходимо добавить код обработчика ключа /P, выполняющий следующие действия:
Firework.Код, реализующий данную функциональность, приведен в листинге 4.22.
static void Main()
{
...
// Если приложение вызвано с ключом вида "/P n"
if ((args.Length == 3) (args[1].ToUpper() == "/P"))
{
// Получаем дескриптор окна предварительного просмотра
IntPtr parentHandle = (IntPtr)uint.Parse(args[2]);
// Определяем координаты клиентской области окна предварительного просмотра
Win32.RECT rect; Win32.GetClientRect(parentHandle, out rect);
// Создаем и заполняем структуру с информацией о классе окна
Win32.WNDCLASSEX wndClassEx = new Win32.WNDCLASSEX();
wndClassEx.cbSize = Marshal.SizeOf(wndClassEx);
wndClassEx.style = (uint)Win32.ClassStyles.CS_PARENTDC;
// Указатель на оконную функцию (см. листинг 4.23).
wndClassEx.lpfnWndProc = new Win32.WndProc(WindowProc);
wndClassEx.cbClsExtra = 0;
wndClassEx.cbWndExtra = 0;
wndClassEx.hIcon = IntPtr.Zero;
wndClassEx.hIconSm = IntPtr.Zero;
wndClassEx.hCursor = IntPtr.Zero;
wndClassEx.hbrBackground = IntPtr.Zero;
wndClassEx.lpszMenuName = null;
wndClassEx.lpszClassName = "XNASCREENSAVER";
wndClassEx.hInstance = Marshal.GetHINSTANCE(typeof(Program) .Module);
// Регистрируем класс окна
if (Win32.RegisterClassEx(ref wndClassEx) == 0)
return;
// Создаем дочернее окно для визуализации хранителя экрана
displayHandle = Win32.CreateWindowEx(0, "XNASCREENSAVER", "XNAScreenSaver",
(uint)(Win32.WindowStyles.WSCHILD | Win32.WindowStyles.WSVISIBLE |
Win32.WindowStyles.WSDISABLED), 0, 0, rect.Width, rect.Height, parentHandle, 4> IntPtr.Zero,
Marshal.GetHINSTANCE(typeof(Program).Module), IntPtr.Zero);
try
{
// Создаем экземпляр класса Firework и передаем ему дескриптор созданного окна. Размер искр и
// частота их появления подобраны таким образом, что фейерверк нормально смотрелся в
// маленьком окошке предварительного просмотра.
firework = new Firework(displayHandle, 1.0f, 5);
}
catch (FireworkException ex)
{
MessageBox.Show(ex.Message, "Критическая ошибка", MessageBoxButtons.OK,
MessageBoxIcon.Error); return;
}
// Запускаем цикл обработки сообщений.
Application.Run(); return;
}
return;
}
}
wmpaint - визуализация изображения.wmclose - освобождение ресурсов и удаление окна.wmdestroy - завершает работу приложения.// Оконная функция
public static IntPtr WindowProc(IntPtr hWnd, uint uMsg, IntPtr wParam, IntPtr lParam)
{
switch (uMsg)
{
// Обработчик сообщения WMPAINT
case (uint)Win32.WindowsMessages.WMPAINT: if
(firework != null)
{
// Обновляем состояние сцены
firework.Update();
// Визуализируем сцену
firework.Paint();
}
return IntPtr.Zero;
// Обработчик сообщения WM_CLOSE
case (uint)Win32.WindowsMessages.WM_CLOSE: if
(firework != null)
{
// Освобождаем ресурсы
firework.Dispose();
firework = null;
}
// Уничтожаем окно
Win32.DestroyWindow(displayHandle);
return IntPtr.Zero;
// Обработчик события WM_DESTROY
case (uint)Win32.WindowsMessages.WM_DESTROY:
// Выходим из цикла обработки сообщений
Win32.PostQuitMessage(0);
return IntPtr.Zero;
default:
return Win32.DefWindowProc(hWnd, uMsg, wParam, lParam);
}
}
(рис 4.9) Визуализация фейерверка в окне предварительного просмотра Для проверки функционирования окна предварительного просмотра скомпилируйте проект хранителя экрана и установите его при помощи контекстного меню. Если все сделано правильно, то на мониторе в окне предварительного просмотра появится фейерверк искр (рисунок 4.9).
После окончания создания хранителя экрана самое время задуматься о его распространении. А именно, о создании дистрибутива, позволяющего неподготовленному пользователю легко инсталлировать и деинсталлировать хранитель экрана. Ведь как гласит народное мудрость, любое приложение встречают по одежке. В принципе, никто не мешает создать дистрибутив прямо на месте, не выходя из Visual Studio 2005. Что мы сейчас и сделаем.
Итак, добавьте в решение новый проект инсталлятора (Add | New Project | Other Project Types | Setup and Deployment | Setup Project). В свойствах ProductName и Manufacture проекта инсталлятора укажите название приложения и организации, которые будет отображаться, к примеру, в окне Add or Remove Programs.
(рис 4.10) Добавление в решение проекта инсталлятора Примечание
Дистрибутив, созданный подобным образом, использует технологию Windows Installer – сервис установки и конфигурирования программных продуктов, являющийся неотъемлемой частью операционных систем Windows 2000 и выше. Хотя данная технология изначально разрабатывалась для развертыванию и сопровождения корпоративного программного обеспечения, она активно используется большинством разработчиков программного обеспечения. Тем не менее "корпоративные корни" дают о себе знать – дистрибутив, использующий Windows Installer, несколько крупнее дистрибутивов, сгенерированных альтернативными инструментами (например, Nullsoft Scriptable Install System ). В прочем эпоху широкополосных каналов Internet и винчестеров объемом в сотни гигабайт лишние 500 килобайт уже не строят погоды.
Все что требуется от нашего инсталлятора – скопировать файл хранителя экрана в каталог Window и выбрать его в качестве текущего хранителя экрана.
По умолчанию папка, в которое устанавливается приложение, расположена внутри каталога Program Files. Чтобы приложение устанавливалось в каталог Windows в окне File System (Setup) щелкните на элементе Application Folder и измените значение свойства Default Location с [ProgramFilesFolder][Manufacturer]\[ProductName] на [WindowsFolder] (рисунок 4.11). Добавьте в папку Application Folder ссылку на *. файл хранителя экрана из каталога ...\bin\Release (эту операцию можно выполнить при помощи команды Add | File… контекстного меню элемента Application Folder ). Обратите внимание, что в папку Application Folder будет автоматически добавлена и ссылка на сборку Microsoft.Xna.Framework.dll, используемую хранителем экрана. В принципе, эту сборку вполне можно исключить из проекта, присвоив свойству Exclude
значение true, однако в этом случае в описание хранителя экрана обязательно нужно указать, что для его нормального функционирования наряду с .NET Framework 2.0 и свежей версией DirectX, требуется установить и Microsoft XNA Framework.
(рис 4.11) Изменение каталога по умолчанию, в которое устанавливается приложения Полный путь исполняемого файла текущего хранителя экрана хранится в значение scrnsave.exe раздела системного реестра HKEYCURRENT_USER\Control Panel\Desktop. Соответственно, для смены текущего хранителя экрана достаточно всего лишь исправить данное значение системного реестра.
Для реализации этой функциональности в окне Solution щелкните правой кнопкой мыши на проекте инсталлятора и выполните команду контекстного меню View | Registry. На экране появится окно с деревом ключей системного реестра. Выберите ключ hkeycurrentuser и создайте в нем ключ Control Panel (команда контекстного меню New | Key ), а в нем ключ Desktop. В ключе Desktop создайте строковое поле scrnsave.exe (команда контекстного меню New | String Value ) и присвойте ему значение вида [TARGETDIR]mysaver. (рисунок 4.12), где
mysaver.scr - имя файла хранителя экрана.Все было просто замечательно, если бы не один нюанс - значение поля scrnsave.exe должно быть коротким именем файла (это ограничение актуально даже для Windows XP Service Pack 2 ). По видимости, это пережиток, оставшийся со времен Windows 3.x, однако с ним приходится считаться. Обойти его в лоб весьма непросто, так не возможно заранее со 100% вероятностью предсказать короткое имя для заданного длинного имени файла. К счастью заботливые разработчики Windows предусмотрели альтернативный способ установки текущего хранителя экрана, не критичный к длине имени файла хранителя экрана.
Этот способ основан на использовании динамической библиотеки desk., которая собственно и реализует окно Display Properties. Данная библиотека экспортирует ряд функций, предоставляющие доступ пакетным файлам, скриптам и прикладным приложениям к функциональности окна Display Properties. В частности функция InstallScreenSaver устанавливает текущий хранитель экрана. Для вызова этой функции можно воспользоваться утилитой rundll32: rundll32.exe desk.
Таким образом, нам необходимо, чтобы инсталлятор по окончанию копирования файлов в каталог Windows вызывал утилиту rundll32, с заданными параметрами.
Технология Windows Installer, используемая Visual Studio, позволяет по завершению установки приложения запустить код из пользовательской сборки, выполняющий некоторые нестандартные действия. Данная функциональность получила называние Custom Actions.
Итак, давайте создадим сборку, которая будет изменять текущий хранитель экрана посредством утилиты rundll32. Для начала добавьте в решение проект сборки новой библиотеки с названием SetupCustomActions (команда контекстного меню Add | New Project… | Visual C# | Class Library ). Щелкните в окне Solution на узле сборки и добавьте класс инсталлятора SetCurrentScrenSaver (Add | New Item… | Installer Class) . В проект будет добавлен новый класс, наследник Installer (листинг 4.24).
using System;
using System.Collections.Generic;
using System.ComponentModel; using
System.Configuration.Install;
namespace SetupCustomActions
{
// Атрибут RunInstaller, установленный в true, указывает на то, что данный класс будет
// автоматически использоваться инсталлятором при установке приложения
[RunInstaller(true)]
public partial class SetCurrentScrenSaver : Installer
{
public SetCurrentScrenSaver()
{
InitializeComponent();
}
}
}
Класс Installer является каркасом, обеспечивающим базовую функциональность Custom Actions. Для добавления новых действий, выполняемых при инсталляции приложения, необходимо переопределить виртуальный метод Install класса Installer (листинг 4.25).
using System.Diagnostics;
public override void Install(System.Collections.IDictionary stateSaver)
{
// Вызываем оригинальный метод класса Installer
base.Install(stateSaver);
// Вызываем утилиту rundll32
Process.Start("rundll32.exe", "desk.cpl,InstallScreenSaver
Context.Parameters["ScreenSaver"]);
}
Информация о местоположении файла получается при помощи свойства Context.Parameters, содержащего ассоциативный массив параметров, переданных данной сборке (передачу параметров в сборку мы рассмотрим чуть ниже).
Скомпилируйте созданную библиотеку классов. Теперь нам необходимо включить полученную сборку в состав дистрибутива. Для этого в окне Solution щелкните правой кнопкой мыши на узле проекта установки ( Setup ) и выполните команду контекстного View | Custom Actions. Откроется окно редактора Custom Actions, содержащее иерархический список действий, которые выполняются при инсталляции приложения, деинсталляции, откате изменений и т.п. Щелкните правой кнопке на узле Install (действия, выполняемые при установке приложения) и выполните команду контекстного меню Add Custom Action…. В появившееся диалоговом окне необходимо выбрать папку, в которую будет скопирована сборка, реализующая Custom Actions. В нашем случае просто выберите в выпадающем списке в верхней части окна папку Application Folder (каталог, в который устанавливается приложение). Затем нажните кнопку Add Output…, и в выпадающем списке Project открывшегося окна выберите проект, содержащий сборку с Custom Actions (как вы помните, мы ее назвали SetupCustomActions ). Наконец, в списке в центральной области окна выберите элемент Primary output (файл, полученный после компиляции указанного проекта) и нажмите Ok (рисунок 4.13).
(рис 4.13) Добавление в дистрибутив сборки, реализующей Custom Actions После выполнения вышеперечисленных действий в окне Custom Actions у элемента Install появится дочерний узел Primary output from SetupCustomActions (Active) . Ну а так как сборка SetupCustomActions содержит класс, производный от Installer, объявленный с атрибутом [RunInstaller(true)] , данный класс будет автоматически использоваться при инсталляции приложения. Правда, просто вызвать класс еще не достаточно – как вы помните, необходимо еще передать сборке параметр с полным именем файла хранителя экрана. Для этого свойству CustomActionData узла Primary output from SetupCustomActions (Active) достаточно присвоить значение / (рисунок 4.14).
(рис 4.14) Задание параметров, передаваемых в сборку
Наш хранитель экрана не является вещью в себе и зависит от ряда компонентов, которых может не оказаться на компьютерах потенциального пользователя. Это
.NET Framework 2.0
XNA Framework 1.0.
Если хотя бы один из этих компонентов не будет установлен на компьютере пользователя, работоспособность хранителя экрана окажется под вопросом. Конечно, можно разместить на диске (или сайте) хранителя экрана дистрибутивы данных компонентов. Однако, ручная установка нескольких компонентов весьма утомляет, кроме того пользователь может банально забыть установить требуемый компонент.
В Visual Studio 2005 эта задача решается путем интеграции необходимых компонентов непосредственно в дистрибутив приложения. В этом случае, при запуске программы установки приложения производится проверка наличия требуемых компонентов с последующей доустановкой недостающих частей.
Данная функциональность реализуется очень просто. Достаточно открыть свойства проекта инсталлятора (команда контекстного меню Properties ), нажать кнопку Prerequisite и в появившемся диалоговом окне выбрать компоненты, которые должны быть установлены на компьютер пользователя (рисунок 4.15). Чтобы поместить выбранные компоненты непосредственно в дистрибутив необходимо установить переключатель Specify the install location for prerequisites в значение Download prerequisites from the same location as my application.
(рис 4.15) Интеграция компонентов в дистрибутив приложения Единственная загвоздка заключается в том, что в состав XNA Game Studio 1.0 Express не входит компонент Prerequisite для Visual Studio 2005. Поэтому вам придется установить его из example.zip. Для этого откройте каталог \Tools\XNA Game Studio Express 1.0\VS 2005 Prerequisite и запустите файл install.bat, после чего в списке диалогового окна Prerequisite появится элемент Microsoft XNA Framework. Если файл install.bat вдруг не сможет обнаружить местоположение Visual Studio, скопируйте вручную подкаталог XNAFramework в \Microsoft Visual Studio 8\SDK\v2.0\BootStrapper\Packages\.
Дополнительная информация
Каталог XNAFramework содержит три файла:
xnafxredist .msi - дистрибутив XNA Framework из XNA Game Studio Express 1.0 (каталог \xna Game Studio Express\v1.0\Redist\XNA FX Redist ).product.xml - описание условий, при которых устанавливается пакет xnafxredist.msi.\En\package.xml - локализация для английского языкаНу что ж, осталось только создать инсталлятор (команда контекстного меню Build ), после чего в каталоге \Setup\Release появится проект готового инсталлятора, который можно смело раздавать своим знакомым без риска быть заваленным вопросами наподобие "что такое XNA Framework и где его взять?".

(рис 4.17) Установка хранителя экрана на компьютер (рис 4.16) Предложение установить на компьютер недостающие компоненты
В этой лекции были подробно рассмотрены все нюансы создания полноценного хранителя экрана, начиная с написания обычного полноэкранного приложения, завершающего работу при активности пользователя, и заканчивая выводом изображения на все мониторы компьютера, реализацией диалогового окна конфигурации хранителя экрана и визуализацией в окне предварительного просмотра. Так же было продемонстрировано применение технологии Windows Installer для создания дистрибутива хранителя экрана, содержащего все необходимые компоненты, включая .NET Framework 2.0 и XNA Framework 1.0.
В этой лекции мы закрепим изученный ранее материал, создав с использованием XNA полнофункциональный хранитель экрана ( ), который отображает на экране фейерверк: вращающийся круг, из которого вылетает множество разноцветных искр (рисунок 4.1).
Что такое хранитель экрана? Это приложение, автоматически запускаемое Windows по истечению определенного срока бездействия компьютера. Изначально использование хранителя экрана было обусловлено сугубо практическими соображения: первые мониторы обладали весьма ограниченным ресурсом, в результате чего длительное отображение статичного изображения приводило к выгоранию люминофора в определенных местах и, соответственно, "запоминанию" монитором этого Norton Commander приводило к выжиганию в люминофоре монитора "изображения" данных панелей
(рис 4.1) Хранитель экранаТехнически хранитель экрана представляет собой исполняемый exe -файл с расширением .. Запуская хранитель экрана, Windows предает ему один из трех параметров командной строки, перечисленных в таблице 4.1. Разработчики C++ обычно создают хранители экрана с использованием библиотеки Scrnsave.lib (или Scrnsavw.lib ), которые самостоятельно обрабатывают параметры командной строки и реализуют завершение работы хранителя экрана при перемещении мыши, нажатии клавиши и т.п. Фактически разработчику необходимо лишь переопределить обработчик события WM_PAINT и реализовать код диалогового окна.
Примечание
К справедливости следует отметить, что реализация диалогового окна средствами Win32 API является далеко не самой приятной задачей.
| Параметр командной строки | Описание |
|---|---|
/s |
Запускает хранитель экрана в обычном режиме |
/c{:n} |
Показывает диалоговое окно конфигурации хранителя экрана. n – дескриптор родительского диалогового окна Display Properties |
/p {n} |
Запускает хранитель экрана в окне предварительного просмотра с дескриптором n. |
| Нет параметров | Показывает диалоговое окно конфигурации хранителя экрана |
В настоящее время .NET Framework 2.0 не содержит аналога библиотеки Scrnsave.lib, поэтому нам придется реализовывать всю функциональность самим. Это далеко не такая тривиальная задача, как хотелось бы, но и отнюдь не архисложная. Так что в путь!
Примечание
В состав Visual Studio 2005 Pro входит Screen Saver Starter Kit, позволяющий создать проект готового хранителя экрана буквально одним щелчком мыши. Но, к сожалению, Screen Saver Starter Kit имеет ряд
Учитывая сложность стоящей перед нами задачи, мы разобьем ее на несколько этапов. Начнем мы с визуализации вращающегося цветного диска. Создайте новый проект Windows Forms, установив в диалоговом окне New Project флажок Create directory for solution (в последствие мы добавим в решение проект инсталлятора). Нам придется выполнять визуализацию как в полноэкранном режиме, так и в окне предварительного просмотра, поэтому весь код визуализации будет логично разместить в отдельном классе. Добавьте в проект новый класс Firework (Project | Add Class).
Внутри класса Firework могут возникать критические исключения (например, при инициализации графического устройства). Обрабатывать в коде, использующем класс Firework, разношерстные исключения вроде ошибки создания графического устройства дольно утомительно, поэтому класс Firework будет перехватывать низкоуровневые исключения и генерировать свое собственное исключение FireworkException с ясным описанием причины возникновения ошибки.
class FireworkException : Exception
{
public FireworkException(string message) : base(message)
{
}
}
Для визуализации круга классу Firework необходимо загрузить и скомпилировать эффект. Только вот где его хранить? Типовой хранитель экрана обычно состоит из одного файла с расширением .fx -файла является далеко не самой лучшей идеей. К счастью Visual Studio позволяет легко внедрить fx -файл непосредственно в exe -файл. Для этого включите в проект файл ColorFill.fx, используемый во всех примерах этой лекции, и присвойте свойству Build Action этого файла значение Embedded Resource (рисунок 4.2).
(рис 4.2) Внедрение fx-файла в сборку Основные фрагменты кода класса Firework приведены в листинге 4.2. Полный текст примера находится в example.zip в каталоге Examples\Ch04\Ex01.
class Firework : IDisposable
{
// Место расположение ресурса с кодом файла эффекта
const string effectFileName = "GSP.XNA.Book.Ch04.Ex01.Data.ColorFill.fx";
// Количество сегментов в диске
const int slices = 64;
// Угловая скорость вращения диска (радиан в секунду)
public const float diskSpeed = 3.0f;
// Радиус диска
public const float diskRadius = 0.015f;
// Текущий угол поворота float diskAngle = 0;
// Дескриптор окна, на которое будет осуществляться визуализация
...
// Конструктор. Принимает:
// hWnd – дескриптор окна в котором осуществляется визуализация.
// scintillaSize – размер искр (пока игнорируется)
// scintillaInterval – интервал между искрами (пока игнорируется)
public Firework(IntPtr hWnd, float scintillaSize,
float scintillaInterval)
{
// Сохраняем дескриптор окна this.hWnd = hWnd;
presentParams = new PresentationParameters();
presentParams.BackBufferCount = 1;
presentParams.SwapEffect = SwapEffect.Discard;
presentParams.PresentationInterval = PresentInterval.One;
try {
GraphicsDeviceCapabilities caps = GraphicsAdapter.
DefaultAdapter.GetCapabilities(DeviceType.Hardware);
CreateOptions options = CreateOptions.SingleThreaded; if
(caps.DeviceCapabilities.SupportsHardwareTransformAndLight)
options |= CreateOptions.HardwareVertexProcessing; else
options |= CreateOptions.SoftwareVertexProcessing;
device = new GraphicsDevice(GraphicsAdapter.DefaultAdapter,
DeviceType.Hardware,
hWnd, options, presentParams);
}
// Это исключение обычно генерируется при отключенном аппаратном
ускорении в Display
// Properties
catch (DeviceNotSupportedException)
{
// Перехватываем исключение и генерируем
собственное исключение с более понятным описанием
// проблемы
throw new FireworkException("Не могу создать устройство Direct3D");
}
decl = new VertexDeclaration(device, VertexPositionColor.VertexElements);
diskVertices = new VertexPositionColor[slices + 2];
// Загружаем эффект из ресурсов сборки
Stream effectStream = Assembly.GetExecutingAssembly().
GetManifestResourceStream(
4> effectFileName) ;
// Выполняем компиляцию эффекта
CompiledEffect compiledEffect = Effect.CompileEffectFromFile
(effectStream, null,
null, CompilerOptions.None, TargetPlatform.Windows);
if (!compiledEffect.Success)
throw new FireworkException(String.Format(
"Ошибка при компиляции эффекта: \r\n{0}", compiledEffect.
ErrorsAndWarnings));
effect = new Effect(device, compiledEffect.GetEffectCode(),
CompilerOptions.NotCloneable, null);
if (!effect.CurrentTechnique.Validate())
throw new FireworkException(String.Format
("Ошибка при валидации техники \"{0}\""+
"эффекта \"{1}\"\n\rСкорее всего,
функциональность шейдера превышает возможности GPU",
effect.CurrentTechnique.Name, effectFileName));
stopwatch = new Stopwatch();
stopwatch.Start();
}
// Рассчитывает угол, на который должен повернуться диск с
момента последнего вызова этого
// метола и выполняет собственно поворот.
public void Update()
{
// Так как хранитель экрана может работать часами, значение
переменной currentTime может
// достигнуть достаточно большой величины. Поэтому, во избежание
падения точности вычислений
// используется тип double
double currentTime = (float)stopwatch.ElapsedTicks /
(float)Stopwatch.Frequency;
// Переменная delta принимает очень ограниченный диапазон
значений, поэтому здесь вполне
// можно обойтись типом float
float delta = (float)(currentTime - lastTime);
// Корректируем угол поворота диска
diskAngle += diskSpeed * delta;
// Рассчитываем новые координаты вершин диска
diskVertices[0] = new VertexPositionColor(new Vector3(0.0f, 0.0f, 0.0f),
XnaGraphics.Color.LightGray);
for (int i = 0; i <= slices; i++) {
float angle = (float)i / (float)slices * 2.0f * (float)Math.PI;
float x = diskRadius * (float)Math.Sin(diskAngle + angle);
float y = diskRadius * (float)Math.Cos(diskAngle + angle);
byte red = (byte)(255 * Math.Abs(Math.Sin(angle * 3)));
byte green = (byte)(255 * Math.Abs(Math.Cos(angle * 2)));
diskVertices[i + 1] = new VertexPositionColor(new Vector3(x, y, 0.0f),
new
XnaGraphics.Color(red, green, 128));
};
lastTime = currentTime;
}
// Выполняет визуализацию изображения
public void Paint()
{
}
// Освобождает ресурсы формы
public void Dispose()
{
if (stopwatch != null)
stopwatch.Stop() ;
if (device != null)
{
device.Dispose() ; device = null;
}
}
}
Теперь, давайте, интегрируем функциональность класса Firework в главную форму приложения (листинг 4.3).
public partial class FullscreenForm : Form
{
Firework firework = null;
public FullscreenForm()
{
InitializeComponent();
}
private void FullscreenFormLoad(object sender, EventArgs e)
{
SetStyle(ControlStyles.Opaque | ControlStyles.ResizeRedraw, true);
MinimumSize = SizeFromClientSize(new Size(1, 1));
try
{
firework = new Firework(Handle, 0.0f, 0.0f);
}
catch (FireworkException ex)
{
MessageBox.Show(ex.Message, "Критическая ошибка", MessageBoxButtons.OK,
MessageBoxIcon.Error);
}
Application.Idle += new EventHandler(ApplicationIdle);
}
void Application_Idle(object sender, EventArgs e)
{
if (firework == null)
{
Close () ; return;
}
firework.Update();
Invalidate() ;
}
private void FullscreenFormFormClosed(object sender, FormClosedEventArgs e)
{
if (firework != null)
{
firework.Dispose(); firework =
null;
}
}
private void FullscreenFormPaint(object sender, PaintEventArgs e)
{
if (firework == null)
return;
firework.Paint();
}
}
В заключение необходимо развернуть форму на весь экран и убрать заголовок. Для этого присвойте свойству FormBorderStyle значение None, а свойству значение Maximized. После запуска полученного приложения экран окрасится в черный цвет, а в центре появится маленький вращающийся пестрый диск.
На следующем этапе мы добавим в наше приложение снопы искр, вылетающие из крутящегося диска. Траектория движения искры, напоминающая спираль, будет складываться как композиция движения искры из центра диска по прямой с постепенным замедлением и движения по окружности вокруг диска с постоянно уменьшающейся угловой скоростью.
Начальный цвет искры будет совпадать с цветом диска в окрестностях появившейся искры. С течением времени искра постепенно затухает посредством уменьшения коэффициента непрозрачности, пока он не достигнет нуля, после чего искра считается потухшей. Всю эту функциональность разумно инкапсулировать в отдельном классе, вложенном в класс Firework (листинг 4.4).
class Scintilla
{
// Время жизни частицы
const float StartTime = 6.0f;
// Диапазон скоростей прямолинейного движения частицы
const float MinSpeed = 0.3f;
const float MaxSpeed = 0.45f;
// Коэффициент замедления прямолинейного движения частицы
const float tSlowing = 0.105f;
// Коэффициент замедления угловой скорости вращательного движения частицы
const float rSlowing = 0.25f;
// Цвет частицы
public Color color;
// Текущие угол поворота частицы вокруг круга и расстояние от центра круга (т.е. координаты
// частицы задаются в сферической системе координат)
public float angle;
public float distance;
// Оставшееся время жизни частицы
public float time;
// Текущая скорость прямолинейного движения
public float tSpeed;
// Текущая скорость вращательного движения
public float rSpeed;
// Возвращает структуру с информацией о частице
public VertexPositionColor Vertex
{
get
{
VertexPositionColor vertex;
// Рассчитываем декартовые координаты частицы
vertex.Position.X = distance * (float)Math.Sin(angle);
vertex.Position.Y = distance * (float)Math.Cos(angle);
vertex.Position.Z = 0;
// Вычисляем новый коэффициент непрозрачности частицы
vertex.Color = new Color(color.R, color.G, color.B,
(byte)(255f * time /
StartTime));
return vertex;
}
}
// Рассчитывает параметры новой частицы. diskAngle – угол поворота диска, используемый для
// синхронизации цвета частицы с цветом диска в окрестностях вершины public void
Init(float diskAngle)
{
// Вычисляем начальное расстояние вершины от центра диска (вершина должна находиться в центре
// диска). Генератор случайных чисел rnd объявлен в классе
Firework
distance = (float)rnd.NextDouble() * Firework.diskRadius;
// Вычисляем начальный угол поворота искры вокруг диска
angle = (float)Firework.rnd.NextDouble() * 2.0f * (float)Math.PI;
// Определяем начальную скорость прямолинейного движения искры
tSpeed = MinSpeed + (float)Firework.rnd.NextDouble() *
(MaxSpeed - MinSpeed);
// Угловая скорость движения вершины всегда в 4 раза меньше
скорости диска rSpeed = Firework.diskSpeed / 4. 0f;
// Рассчитываем начальный цвет искры (он должен совпадать с цветом диска)
byte red = (byte)(255 * Math.Abs(Math.Sin((angle - diskAngle) * 3)));
byte
green = (byte)(255 * Math.Abs(Math.Cos((angle - diskAngle) * 2)));
color =
new Color(red, green, 128, 255);
// Задаем оставшееся время жизни искры time
= StartTime;
}
// Обновляет состояние искры. delta - время, прошедшее с момента
последнего вызова метода
// Update
public void Update(float delta)
{
// Если искра еще не потухла if (time gt;
0.0f)
{
// Уменьшаем оставшееся время жизни искры
time = time - delta;
// Корректируем скорости прямолинейного и вращательного движений
tSpeed = Math.Max(tSpeed - tSlowing * delta, 0.0f); rSpeed =
Math.Max(rSpeed - rSlowing * delta, 0.0f);
// Корректируем положение искры в пространстве distance += tSpeed *
delta;
angle += rSpeed * delta;
}
}
}
Пользователь сможет управлять видом фейерверка искр посредством двух параметров:
Так как количество искр на экране постоянно меняется, встает вопрос хранения информации об искрах. В принципе, для этой цели вполне подходит обобщенный класс List за исключением пары нюансов:
DrawUserPrimitives может визуализировать вершины исключительно из массивов. Однако преобразование списка List в массив посредством метода ToArray() сопряжено с выделением памяти, что в свою очередь может привести к частым вызовам сборщика мусора и, соответственно, провалам производительности.Первый недостаток мы обойдем достаточно хитро. Потухшие искры не будут удаляться из списка - вместо этого информация о новых искрах будет просто заноситься в элементы списка с потухшими искрами, затирая их, и лишь при отсутствии таковых добавляться в конец списка. Для борьбы со вторым недостатком мы будем самостоятельно копировать содержимое списка в массив, при этом выделение памяти для массива будет производиться лишь при недостаточном размере целевого массива. В листинге 4.5 приведены фрагменты обновленного класса Firework с учетом вышеприведенных требований.
class Firework : IDisposable
{
// Дискретный шаг времени, с которым выполняется моделирование логики работы хранителя экрана
const float timeStep = 0.005f;
// Максимальное время между вызовами Update не должно превышать 1 секунды. Большие временные
// интервалы игнорируются
const float maxDelta = 1.0f;
// Максимальное количество искр, которое может появиться за один дискретный шаг времени (5
// миллисекунд)
int maxScintillaCount;
// Вероятность появления следующей искры
const float scintillaProbability = 0.3f;
// Размер искры
float scintillaSize;
...
// Время, прошедшее с момента запуска приложения
double lastTime = 0;
// Список искр
List<Scintilla> scintillas;
// Массив вершин для визуализации искр
VertexPositionColor[] scintillasVertices = null;
// Количество вершин, хранящихся в массиве
int scintillasVertexNount = 0;
// Генератор случайных чисел, используемый классами Firework и Scintilla
public static Random rnd = new Random();
// Конструктор
public Firework(IntPtr hWnd, float scintillaSize, float scintillaInterval)
{
this.hWnd = hWnd;
// Запоминаем пользовательские настройки
this.scintillaSize = scintillaSize;
this.maxScintillaCount = maxScintillaCount;
...
// Создаем список вершин
scintillas = new List<Scintilla>(16);
// Создаем массив вершин
scintillasVertices = new VertexPositionColor[16];
...
}
// Обновляет сцену
public void Update()
{
double currentTime = (float)stopwatch.ElapsedTicks / (float)Stopwatch.Frequency;
// Максимальный временной интервал не должен превышать maxDelta if (currentTime –
lastTime > maxDelta) lastTime = currentTime - maxDelta;
// Количество дискретных шагов timeStep, которые необходимо выполнить
int stepCount = (int)Math.Floor((currentTime - lastTime) / timeStep);
// Интервал между двумя вызовами метода Update, с учетом дискретности времени float
delta = stepCount * timeStep;
// Поворачиваем диск
diskAngle += diskSpeed * delta;
// Корректируем положение вершин диска
...
// Моделируем движение искр с дискретным шагом времени for (int i =
0; i < stepCount; i++)
{
lastTime += timeStep;
// Счетчик количества не потухших искр scintillasVertexСount = 0;
// Количество новых искр, которые могут появиться на данном шаге
int scintillaCount = rnd.Next(maxScintillaCount + 1);
// Перебираем все искры
for (int j = 0; j < scintillas.Count; j++)
{
// Обновляем состояние текущей искры
scintillas[j].Update(delta);
// Если искра не потухла
if (scintillas[j].time gt; 0)
{
// Увеличиваем счетчик не потухших искр
scintillasVertexСount++;
}
else
// Если искра является потухшей
{
// Пока не исчерпан лимит новых искр
while (scintillaCount gt; 0)
{
// Пробуем добавить новую искру, поэтому уменьшаем счетчик искр
scintillaCount--;
// Генерируем новую искру с вероятностью scintillaProbability
if ((Firework.rnd.NextDouble() lt; scintillaProbability))
{
// Инициализируем текущую искру
scintillas[j].Init(diskAngle);
// Увеличиваем счетчик искр
scintillasVertexСount++; break;
}
}
}
}
// Если необходимо создать еще несколько новых искр while
(scintillaCount gt; 0)
{
scintillaCount--;
if ((Firework.rnd.NextDouble() lt; scintillaProbability))
{
// Добавляем в список информацию о новой искре
scintillas.Add(new Scintilla());
scintillas[scintillas.Count - 1].Init(diskAngle);
scintillasVertexNount++;
}
}
}
// Если число искр превышает размер массива вершин
if (scintillasVertexNount > scintillasVertices.Length)
{
// Удваиваем размер массива. Если размер удвоенного массива недостаточен, используем в
// качестве размера массива текущее количество вершин (на всякий случай перестраховываемся)
scintillasVertices = new VertexPositionColor[Math.Max(scintillasVertexNount,
scintillasVertices.Length * 2)];
}
// Копируем информацию о искрах в массив вершин int
k = 0;
for (int i = 0; i < scintillas.Count; i++)
// Учитывает только не потухшие искры if
(scintillas[i].time > 0)
{
scintillasVertices[k] = scintillas[i].Vertex; k++;
}
}
// Визуализация сцены public
void Paint()
{
device.RenderState.CullMode = CullMode.None;
// Задаем режим смешения пикселей для моделирования полупрозрачности
device.RenderState.AlphaBlendEnable = true;
device.RenderState.BlendFunction = BlendFunction.Add;
device.RenderState.SourceBlend = Blend.SourceAlpha;
device.RenderState.DestinationBlend = Blend.InverseSourceAlpha;
// Задаем размер точек (искр)
device.RenderState.PointSize = scintillaSize;
// Визуализируем массив искр
if (scintillasVertexNount > 0)
{
device.DrawUserPrimitives(PrimitiveType.PointList, scintillasVertices, 0,
scintillasVertexNount);
}
// Визуализируем вращающийся диск
device.DrawUserPrimitives(PrimitiveType.TriangleFan, diskVertices, 0,
4> diskVertices.Length - 2);
}
Обработчик события Load формы также нуждается в косметической правке:
private void FullscreenFormLoad(object sender, EventArgs e)
{
// Задаем размеры искр и максимальное количество искр, генерируемое каждые 5 миллисекунд
firework = new Firework(Handle, 3.0f, 10f);
}
Ну что ж, настало время подумать о преобразовании приложения в хранитель экрана. Как известно, любой хранитель экран должен автоматически завершать работу приложения при активности пользователя. Начнем с клавиатуры - нажатие любой клавиши клавиатуры должно немедленно завершать работу приложения (листинг 4.6).
private void FullscreenFormKeyDown(object sender, KeyEventArgs e)
{
Close () ;
}
Аналогичным образом приложение должно завершать работу при нажатии кнопки мыши или перемещении курсора мыши. Но здесь есть одна тонкость. Дело в том, что курсор мыши с высокой чувствительностью может реагировать даже на незначительные воздействия вроде микроколебаний стола из-за проезжающего за окном поезда. Поэтому во избежание непреднамеренных прерываний хранителя экрана мы будет завершать работу приложения только после того, как курсор отодвинется от первоначального положения в момент активации хранителя экрана на расстояние порядка 10 пикселей (листинг 4.7).
public partial class FullscreenForm : Form
{
// Флаг, устанавливаемый в true после первого вызова обработчика события MouseMove
bool isMouseActive = false;
// Координаты мыши при первом вызове обработчика события MouseMove
System.Drawing.Point mouseLocation;
...
// Обработчик события MouseDown, завершающий работу приложения при нажатии кнопки мыши
private void FullscreenForm_MouseDown(object sender, MouseEventArgs e)
{
Close();
}
// Обработчик события MouseMove
private void FullscreenForm_MouseMove(object sender, MouseEventArgs e)
{
// Обработчик события MouseMove запускается впервые if (!isMouseActive)
{
isMouseActive = true;
// Запоминаем текущие
координаты мыши
mouseLocation = e.Location;
}
else
{
// Если курсор мыши переместился вдоль оси X или Y от своего первоначального положения
// больше, чем на 10 единиц
if ((Math.Abs(e.Location.X - mouseLocation.X) > 10) ||
(Math.Abs(e.Location.Y - mouseLocation.Y) > 10))
{
// Завершаем работу приложения Close();
}
}
}
}
В процессе работы хранителя экрана некоторое приложение может вывести на экран диалоговое окно с важной информацией (например, Internet Explorer по окончанию загрузки файла). При этом окно хранителя экран теряет фокус, который переходит к новому диалоговому окну. Хранитель экрана, поверх которого отображается диалоговое окно, будет выглядеть, мягко говоря, несколько странно, поэтому в качестве одного из критериев завершения работы хранителя экрана логично использовать потерю фокуса формой (листинг 4.8).
// Обработчик события Deactivate полноэкранной формы хранителя экран, завершающий работу
// приложения при потере формой фокуса
private void FullscreenFormDeactivate(object sender, EventArgs e)
{
Close () ;
}
Курсор мыши является чужеродным элементом для хранителя экрана, поэтому его необходимо скрыть посредством метода Hide класса Cursor:
private void FullscreenFormLoad(object sender, EventArgs e)
{
Cursor.Hide();
}
Следующее отличие хранителя экрана от обычного приложения состоит в том, что он должен активироваться только при запуске приложения с ключом /s. Соответственно, мы должны добавить в метод Main статического класса Program анализ параметров командной строки (листинг 4.10).
Примечание
Чтобы Visual Studio всегда запускала приложение с ключом /s, укажите этот параметр в поле Command line arguments вкладки Debug свойств проекта.
static class Program
{
[STAThread]
static void Main()
{
// Получаем массив параметров командной
строки
string[] args = Environment.GetCommandLineArgs();
// Если первый параметр равен "/S"
if ((args.Length == 2) (args[1].ToUpper() == "/S"))
{
// Отображаем форму приложения с хранителем экрана
Application.Run(new FullscreenForm()); return;
}
// Если параметр не является "/S", нечего не делаем
return;
}
}
В заключении необходимо присвоить файлу хранителя экрана расширение .. Переименовывать файл вручную после каждой компиляции приложения довольно утомительно, поэтому мы автоматизируем этот процесс. Откройте в свойствах проекта вкладку Build Events и введите в поле Post-build event command line следующую команду (рисунок 4.3): copy "$(TargetFileName)" "*..
Теперь после каждой компиляции приложения будет вызываться команда copy, создающая копию exe-файла приложения с расширением .. Обратите внимание на получение имени exe-файла приложения посредством встроенного макроса $(TargetFileName) , благодаря чему команда copy не привязана к фиксированному exe -файлу.
(рис 4.3) Вкладка Build Events Для проверки работоспособности хранителя экрана откройте каталог с . -файлом в файловом менеджере и вызовите его контекстное меню (рисунок 4.4). Как видно, контекстное меню любого исполняемого файла хранителя экрана содержит три пункта:
Test - запускает хранитель экрана на выполнение с ключом /s.Configure (Настроить) - открывает окно конфигурации хранителя экрана.Install (Установить) - открывает вкладку Screen Saver диалогового окна Display Properties и выбирает данный хранитель экрана в качестве текущего.Немного проигравшись с нашим хранителем экрана, вы заметите ряд недоделок. Например, при попытке открыть окно конфигурации ровным счетом нечего не происходит, а в окне предварительного просмотра (маленький "дисплейчик") диалогового окна Display Properties просто выводится изображение по умолчанию. А на компьютере с несколькими мониторами выяснится, что наш хранитель экрана активируется только основном мониторе. Что ж, работы нам предстоит еще много.
(рис 4.4) Контекстное меню исполняемого файла хранителя экрана
В настоящее время поддержка видеокартами двух мониторов уже стала нормой, поэтому любой уважающий себя разработчик должен позаботиться о корректном функционировании приложения на компьютере с несколькими мониторами. В частности, хранитель экрана должен показывать заставку на всех мониторах. Наиболее простое решение - просто отображать на всех мониторах одно и то же изображение. Так как наш хранитель экрана представляет собой форму, развернутую на весь экран, в случае нескольких мониторов мы можем просто создать несколько экземпляров формы - по одному на каждый монитор.
Начнем с метода Main. Информация об экранных координатах всех мониторов системы храниться в коллекции AllScreens класса Screen. Соответственно приложение должно просто перебрать элементы этой коллекции и использовать полученную информацию при создании форм (листинг 4.11).
static void Main()
{
string[] args = Environment.GetCommandLineArgs();
if ((args.Length == 2) (args[1].ToUpper() == "/S"))
{
// Перебираем все мониторы
foreach (Screen screen in Screen.AllScreens)
{
// Создаем форму размеров во весь монитор
FullscreenForm form = new FullscreenForm(screen);
// Отображаем форму
form. Show () ;
}
// Запускаем цикл обработки сообщений. Изображение форм будет обновляться посредством
// обработчиков события Idle, регистрируемых конструктором формы. Application.Run();
return;
}
return;
}
Конструктор формы, разумеется, так же придется подправить, ведь теперь он будет принимать информацию об экране, на котором будет отображаться форма. Вступать же в силу данный параметр будет после конструирования формы в обработчике события Load (листинг 4.12).
Screen screen = null;
public FullscreenForm(Screen screen)
{
this.screen = screen;
InitializeComponent();
}
private void FullscreenFormLoad(object sender, EventArgs e)
{
// Форма должна занимать весь экран
// Внимание! Свойство Bounds не оказывает влияния, если форма развернута на весь экран
// (т.е. когда свойство WindowsState равно Maximized) Bounds = screen.Bounds;
}
Наконец необходимо определиться с завершением работы. До сих пор все наши приложения содержали лишь одно главное окно, закрытие которого методом Close приводило к завершению работы всего приложения. Теперь же окон несколько, поэтому вызов метода Close закроет лишь единственное окно. Поэтому мы будет завершать работу приложения путем вызова метода Application.Exit. Правда у этого подхода есть один подводный камень - при завершении работы методом Application.Exit не вызываются обработчики события FormClosed. Поэтому код из обработчиков необходимо перенести в обработчики события FormClosing, корректно вызываемых методом Application.Exit. Другой нюанс связан с обработчиком события Deactivate: так мы создаем несколько форм, в процессе создания они будут неминуемо получать-терять фокус (ведь в каждый момент времени только одна форма может иметь фокус). Поэтому во избежание досрочного завершения хранителя экра
на в процессе инициализации приложения необходимо игнорировать событие Deactivate. Основные фрагменты обновленных обработчиков событий формы приведены в листинге 4.13.
// Ресурсы теперь освобождаются в обработчике события FormClosing
private void FullscreenFormFormClosing(object sender, FormClosingEventArgs e)
{
if (firework != null)
{
firework.Dispose() ; firework =
null;
}
}
private void FullscreenFormDeactivate(object sender, EventArgs e)
{
// Пока в приложение не запущен цикл обработки сообщений, игнорируем событие Deactivate if
(Application.MessageLoop) Application.Exit();
}
private void FullscreenForm_MouseDown(object sender, MouseEventArgs e)
{
// Обратите внимание на завершение приложения посредством метода Application.Exit (вместо
// Form.Close)
Application.Exit();
}
...
Готовое приложение можно найти в example.zip в каталоге Examples\Ch04\Ex04.
Настало время подумать об управлении пользовательскими настройками хранителя экрана. Работа нашего хранителя экрана управляется двумя параметрами: размером искр и максимальным числом искр, вылетающих в течение кванта времени, равного 5 миллисекунд. Второй параметр не является интуитивно понятным, ведь рядовому пользователю намного проще регулировать количество искр посредством ползунка "мало – много". Поэтому мы пойдем на небольшую хитрость: количество искр будет задаваться целочисленным параметром "плотность искр", лежащим в диапазоне от 0 (минимальное количество искр) до 9 (максимальное количество искр), а число искр, появляющихся каждые 5 секунд, будет рассчитываться уже на основе данного целочисленного параметра.
Итак, откройте вкладку настроек приложения (Properties | Setting) и добавьте в него два целочисленных параметра (рисунок 4.5):
(рис 4.5) Вкладка настроек приложения scintillaSize - размер искр. Значение по умолчанию 2scintillaDensity - плотность искр. Значение по умолчанию 5.Для начала немного подкорректируйте обработчик события Load полноэкранной формы хранителя экрана, чтобы он брал настройки непосредственно из файла конфигурации приложения (листинг 4.14).
public partial class FullscreenForm : Form
{
Properties.Settings settings;
private void FullscreenForm_Load(object sender, EventArgs e)
{ ...
// Загружаем настройки приложения из файла конфигурации (или настройки по умолчанию при
// отсутствии файла)
settings = new Properties.Settings();
try
{
// Количество искр, генерируемых каждые 5 мс, рассчитывается методом "научного
тыка" firework = new Firework(Handle, (float)settings.scintillaSize,
(settings.scintillaDensity + 1) * 5);
}
...
}
Теперь создайте новую форму и поместите на нее компоненты согласно рисунку 4.6 и таблице 4.2.
(рис 4.6) Диалоговое окно параметры.| Класс | Свойство | Значение |
|---|---|---|
SettingsForm (диалоговое окно) |
Name |
SettingsForm |
Text |
Параметры | |
ShowInTaskbar |
false |
|
TopMost |
true |
|
MinimizeBox |
false |
|
MaximizeBox |
false |
|
GroupBox |
Text |
Искры |
Label |
Text |
Размер |
TrackBar |
Name |
scintillaSizeTrackBar |
Minimum |
1 | |
Maximum |
4 | |
Label |
Text |
Плотность |
TrackBar |
Name |
scintillaDensityTrackBar |
Minimum |
0 | |
Maximum |
9 | |
Button |
Name |
okButton |
Text |
Ok |
|
Button |
Name |
cancelButton |
Text |
Отмена |
Для автоматической инициализации элементов управления диалогового окна ползунки scintillaSizeTrackBar и scintillaDensityTrackBar необходимо связать со свойствами scintillaSize и scintillaDensity из конфигурационного файла приложения. Это операция легко выполняется посредством свойства Application Settings | Value ползунков (рисунок 4.7).
(рис 4.7) Привязка значения ползунка scintillaSizeTrackBar к свойству scintillaSize файла конфигурации Следующий шаг - оживление формы посредством реализации нехитрых обработчиков сообщений (листинг 4.15).
public partial class SettingsForm : Form
{
public SettingsForm()
{
InitializeComponent();
}
// Обработчик нажатия кнопки Ok
private void okButton_Click(object sender, EventArgs e)
{
Properties.Settings settings = new Properties.Settings();
// Задаем новые значения свойств файла конфигурации
settings.scintillaSize = scintillaSizeTrackBar.Value;
settings.scintillaDensity = scintillaDensityTrackBar.Value;
// Сохраняем информацию в файле
settings.Save();
// Закрываем форму и завершаем приложение
Close ();
}
// Обработчик нажатия кнопки Отмена
private void cancelButtonClick(object sender, EventArgs e)
{
// Просто завершаем приложение, не сохраняя изменения
Close ();
}
}
Как говорилось в начале лекции, диалоговое окно настроек хранителя экрана должно отображаться в 3-х случаях:
/C./C:n, где n - дескриптор диалогового окна Display Properties. Например, /C:299792.Для начала, мы можем попробовать добавить в функцию Main статического класса Program следующий код:
static void Main()
{
// Если приложение запущено без параметров или первый параметр начинается на "/C"
if ((args.Length == 1) || ((args.Length == 2)
(args [1] .Length >= 2) (args [1] .ToUpper () .Substring(0, 3) == "/C")))
{
// Отображаем диалоговое окно
Application.Run(new SettingsForm());
return;
}
}
В принципе, подобный подход является вполне работоспособным, если не считать одной особенности: диалоговое окно может появиться на совершенно другом конце экрана (и даже на другом мониторе) относительно окна Display Properties. Для хранителя экрана, сделанного на профессиональном уровне, такое поведение не допустимо, поэтому нам необходимо решить данную проблему.
(рис 4.8) Начальное положение диалоговое окна Параметры не связано с текущим положением окна Display Properties Для выравнивания диалогового окна по центру окна Display Properties необходимо определить положение этого самого окна Display Properties на экране. Здесь самое время вспомнить о том, что при запуске хранителя экрана диалоговое окно Display Properties передает ему в качестве параметра свой дескриптор. Ну а немного поколдовав с Win32 API над дескриптором окна, можно легко получить об этом окне практически любую информацию.
Для начала мы добавим в класс диалогового окна еще один конструктор, принимающий в качестве параметра дескриптор окна Display Properties (листинг 4.17).
public partial class SettingsForm : Form
{
// Необходимо ли центрировать окно параметров относительно окна Display Properties. По
// умолчанию центрирование не выполняется
bool center = false;
// Дескриптор окна Display Properties
IntPtr parentHandle;
public SettingsForm(IntPtr parentHandle):this()
{
// Запоминаем дескриптор
this.parentHandle
= parentHandle;
// Включаем центрирование
center = true;
}
}
Чтобы задействовать новый конструктор в коде функции Main необходимо реализовать более детальный разбор параметров с выделением из ключа вида /C:n значения дескриптора (листинг 4.18).
static void Main()
{
// Если приложение запущено без параметров (такое происходит, к примеру, при открытии окна
// конфигурации при помощи контекстного меню) или с ключом "/C"
if ((args.Length == 1) || ((args.Length >= 2) args[1].ToUpper() == "/C"))
{
// Открываем диалоговое окно параметров приложения без центрирования
Application.Run(new SettingsForm()); return;
}
// Если параметр хранителя экрана имеет вид /C:n
if ((args.Length == 2) (args[1].Length > 3)
(args[1].ToUpper().Substring(0, 3) == "/C:"))
{
// Выделяем из строки дескриптор окна и преобразуем его в значение типа IntPtr
IntPtr hWnd = (IntPtr)int.Parse(args[1].ToUpper().Substring(3, args[1].Length - 3));
// Открываем диалоговое окно с выравниванием по центру окна Display Properties с дескриптором
// hWnd
Application.Run(new SettingsForm(hWnd));
}
}
Переходим к самому интересному - получению информации о положении окна по его дескриптору. Немного порывшись в MSDN мы обнаружим требуемую нам функцию:
BOOL GetWindowRect(HWND hWnd, LPRECT lpRect);
где
hWnd - дескриптор окна;lpRect - указатель на структуру rect, в которую заносятся координаты верхнего левого и нижнего правого углов окна.К сожалению . Так как методы Win32 API наверняка еще не один раз пригодиться нам, будет разумно вынести их в отдельный статический класс Win32 (листинг 4.19). Дополнительным плюсом подобного подхода является простая идентификация в тексте программы вызовов методов Win32, являющихся потенциальными источниками проблем при переносе приложения на другие платформы (например, на x64 или Xbox 360 ).
// Определения функций Win32 API, написанные на основе
материалов сайта http://pinvoke.net
using System.Runtime.InteropServices;
public static class Win32
{
// Структура RECT
[StructLayout(LayoutKind.Sequential)]
public struct RECT
{
public int left; public
int top; public int
right; public int
bottom;
// Добавляем "от себя" два свойства, позволяющие
легко определить ширину и высоту
области public int Width { get { return right - left + 1;
}
}
public int Height { get
{
return bottom - top + 1;
}
}
}
// Возвращает координаты окна
[DllImport("user32.dll")]
public static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
}
И, наконец, последний штрих - реализация в обработчике Load диалогового окна выравнивания по центру окна Display Properties (листинг 4.20).
private void SettingsForm_Load(object sender, EventArgs e)
{
if (center)
{
// Создаем структуру RECT
Win32.RECT rect = new Win32.RECT();
// Получаем информацию о местоположении окна Display Properties
Win32.GetWindowRect(parentHandle, out rect);
// Вычисляем координаты центра окна Display Properties int
centerX = (rect.right + rect.left) / 2; int centerY
= (rect.bottom + rect.top) / 2;
// Позиционируем наше диалоговое окно по центру окна Display Properties
Left = centerX - Width / 2;
Top = centerY - Height / 2;
}
}
Готовое приложение можно найти в example.zip в каталоге Examples\Ch04\Ex05.
Переходим к заключительному и самому нетривиальному этапу – отображению хранителя экрана в окне предварительного просмотра. Чтобы реализовать эту функциональность приложение должно получить из командной строки дескриптор области предварительного просмотра (мониторчик на вкладке Screen Saver окна Display Properties ) и создать в этом окне свой элемент управления. Для анимации созданного элемента управления окно Display Properties будет автоматически посылать ему сообщения WM_PAINT, а при выборе другого хранителя экрана, смене вкладки или закрытии диалогового окна Display Properties – сообщение WM_CLOSE.
К сожалению, большую часть этой функциональности не возможно реализовать средствами .NET Framework поэтому на придется опуститься до уровня оконных процедур и циклов обработки сообщений Win32. Для начала определим все необходимые константы, структуры и функции Win32 (листинг 4.21).
public static class Win32
{
...
// Определение делегата оконной функции обработки сообщений.
public delegate IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);
// Структура WNDCLASSEX, используемая при регистрации класса окна
[StructLayout(LayoutKind.Sequential)] public struct WNDCLASSEX
{
[MarshalAs(UnmanagedType.U4)]
public int cbSize;
[MarshalAs(UnmanagedType.U4)]
public uint style;
public WndProc lpfnWndProc;
public int cbClsExtra;
public int cbWndExtra;
public IntPtr hInstance;
public IntPtr hIcon;
public IntPtr hCursor;
public IntPtr hbrBackground;
public string lpszMenuName;
public string lpszClassName;
public IntPtr hIconSm; }
// Битовые флаги стилей окна [Flags]
public enum WindowStyles : uint
{
// Окно является дочерним
WS_CHILD = 0x40000000,
// Окно сразу же является видим
WS_VISIBLE = 0x10000000,
// Окно игнорирует действия пользователя
WS_DISABLED = 0x08000000,
...
}
// Битовые флаги стилей класса окна
[Flags]
public enum ClassStyles : uint
{
// Окно будет использовать контекст устройства родительского окна
CS_PARENTDC = 0x0080,
...
}
// Идентификаторы сообщений Windows
public enum WindowsMessages : uint
{
WM_CLOSE = 0x10, WM_DESTROY = 0x2,
WM_PAINT = 0xF,
...
}
// Регистрирует класс окна [DllImport("user32")] public static extern short
RegisterClassEx([In] ref WNDCLASSEX lpwcx);
// Создает новое окно
[DllImport("user32.dll")]
public static extern IntPtr CreateWindowEx(uint dwExStyle, string lpClassName,
4> string lpWindowName, uint dwStyle, int x, int y, int nWidth, int nHeight, 4> IntPtr
hWndParent, IntPtr hMenu, IntPtr hInstance, IntPtr lpParam);
// Возвращает координаты клиентской области окна [DllImport("user32.dll")]
public static extern bool GetClientRect(IntPtr hWnd, out RECT lpRect);
// Уничтожает окно
[DllImport("user32.dll")] public static extern bool
DestroyWindow(IntPtr hWnd);
// Помещает в очередь сообщений WMQUIT, завершающее выполнение цикла обработки сообщений
[DllImport("user32.dll")] public static extern void PostQuitMessage(int nExitCode);
// Вызывает обработчик сообщения по умолчанию
[DllImport("user32.dll")]
public static extern IntPtr DefWindowProc(IntPtr hWnd, uint uMsg, IntPtr wParam, IntPtr
lParam);
}
В функцию Main необходимо добавить код обработчика ключа /P, выполняющий следующие действия:
Firework.Код, реализующий данную функциональность, приведен в листинге 4.22.
static void Main()
{
...
// Если приложение вызвано с ключом вида "/P n"
if ((args.Length == 3) (args[1].ToUpper() == "/P"))
{
// Получаем дескриптор окна предварительного просмотра
IntPtr parentHandle = (IntPtr)uint.Parse(args[2]);
// Определяем координаты клиентской области окна предварительного просмотра
Win32.RECT rect; Win32.GetClientRect(parentHandle, out rect);
// Создаем и заполняем структуру с информацией о классе окна
Win32.WNDCLASSEX wndClassEx = new Win32.WNDCLASSEX();
wndClassEx.cbSize = Marshal.SizeOf(wndClassEx);
wndClassEx.style = (uint)Win32.ClassStyles.CS_PARENTDC;
// Указатель на оконную функцию (см. листинг 4.23).
wndClassEx.lpfnWndProc = new Win32.WndProc(WindowProc);
wndClassEx.cbClsExtra = 0;
wndClassEx.cbWndExtra = 0;
wndClassEx.hIcon = IntPtr.Zero;
wndClassEx.hIconSm = IntPtr.Zero;
wndClassEx.hCursor = IntPtr.Zero;
wndClassEx.hbrBackground = IntPtr.Zero;
wndClassEx.lpszMenuName = null;
wndClassEx.lpszClassName = "XNASCREENSAVER";
wndClassEx.hInstance = Marshal.GetHINSTANCE(typeof(Program) .Module);
// Регистрируем класс окна
if (Win32.RegisterClassEx(ref wndClassEx) == 0)
return;
// Создаем дочернее окно для визуализации хранителя экрана
displayHandle = Win32.CreateWindowEx(0, "XNASCREENSAVER", "XNAScreenSaver",
(uint)(Win32.WindowStyles.WSCHILD | Win32.WindowStyles.WSVISIBLE |
Win32.WindowStyles.WSDISABLED), 0, 0, rect.Width, rect.Height, parentHandle, 4> IntPtr.Zero,
Marshal.GetHINSTANCE(typeof(Program).Module), IntPtr.Zero);
try
{
// Создаем экземпляр класса Firework и передаем ему дескриптор созданного окна. Размер искр и
// частота их появления подобраны таким образом, что фейерверк нормально смотрелся в
// маленьком окошке предварительного просмотра.
firework = new Firework(displayHandle, 1.0f, 5);
}
catch (FireworkException ex)
{
MessageBox.Show(ex.Message, "Критическая ошибка", MessageBoxButtons.OK,
MessageBoxIcon.Error); return;
}
// Запускаем цикл обработки сообщений.
Application.Run(); return;
}
return;
}
}
wmpaint - визуализация изображения.wmclose - освобождение ресурсов и удаление окна.wmdestroy - завершает работу приложения.// Оконная функция
public static IntPtr WindowProc(IntPtr hWnd, uint uMsg, IntPtr wParam, IntPtr lParam)
{
switch (uMsg)
{
// Обработчик сообщения WMPAINT
case (uint)Win32.WindowsMessages.WMPAINT: if
(firework != null)
{
// Обновляем состояние сцены
firework.Update();
// Визуализируем сцену
firework.Paint();
}
return IntPtr.Zero;
// Обработчик сообщения WM_CLOSE
case (uint)Win32.WindowsMessages.WM_CLOSE: if
(firework != null)
{
// Освобождаем ресурсы
firework.Dispose();
firework = null;
}
// Уничтожаем окно
Win32.DestroyWindow(displayHandle);
return IntPtr.Zero;
// Обработчик события WM_DESTROY
case (uint)Win32.WindowsMessages.WM_DESTROY:
// Выходим из цикла обработки сообщений
Win32.PostQuitMessage(0);
return IntPtr.Zero;
default:
return Win32.DefWindowProc(hWnd, uMsg, wParam, lParam);
}
}
(рис 4.9) Визуализация фейерверка в окне предварительного просмотра Для проверки функционирования окна предварительного просмотра скомпилируйте проект хранителя экрана и установите его при помощи контекстного меню. Если все сделано правильно, то на мониторе в окне предварительного просмотра появится фейерверк искр (рисунок 4.9).
После окончания создания хранителя экрана самое время задуматься о его распространении. А именно, о создании дистрибутива, позволяющего неподготовленному пользователю легко инсталлировать и деинсталлировать хранитель экрана. Ведь как гласит народное мудрость, любое приложение встречают по одежке. В принципе, никто не мешает создать дистрибутив прямо на месте, не выходя из Visual Studio 2005. Что мы сейчас и сделаем.
Итак, добавьте в решение новый проект инсталлятора (Add | New Project | Other Project Types | Setup and Deployment | Setup Project). В свойствах ProductName и Manufacture проекта инсталлятора укажите название приложения и организации, которые будет отображаться, к примеру, в окне Add or Remove Programs.
(рис 4.10) Добавление в решение проекта инсталлятора Примечание
Дистрибутив, созданный подобным образом, использует технологию Windows Installer – сервис установки и конфигурирования программных продуктов, являющийся неотъемлемой частью операционных систем Windows 2000 и выше. Хотя данная технология изначально разрабатывалась для развертыванию и сопровождения корпоративного программного обеспечения, она активно используется большинством разработчиков программного обеспечения. Тем не менее "корпоративные корни" дают о себе знать – дистрибутив, использующий Windows Installer, несколько крупнее дистрибутивов, сгенерированных альтернативными инструментами (например, Nullsoft Scriptable Install System ). В прочем эпоху широкополосных каналов Internet и винчестеров объемом в сотни гигабайт лишние 500 килобайт уже не строят погоды.
Все что требуется от нашего инсталлятора – скопировать файл хранителя экрана в каталог Window и выбрать его в качестве текущего хранителя экрана.
По умолчанию папка, в которое устанавливается приложение, расположена внутри каталога Program Files. Чтобы приложение устанавливалось в каталог Windows в окне File System (Setup) щелкните на элементе Application Folder и измените значение свойства Default Location с [ProgramFilesFolder][Manufacturer]\[ProductName] на [WindowsFolder] (рисунок 4.11). Добавьте в папку Application Folder ссылку на *. файл хранителя экрана из каталога ...\bin\Release (эту операцию можно выполнить при помощи команды Add | File… контекстного меню элемента Application Folder ). Обратите внимание, что в папку Application Folder будет автоматически добавлена и ссылка на сборку Microsoft.Xna.Framework.dll, используемую хранителем экрана. В принципе, эту сборку вполне можно исключить из проекта, присвоив свойству Exclude
значение true, однако в этом случае в описание хранителя экрана обязательно нужно указать, что для его нормального функционирования наряду с .NET Framework 2.0 и свежей версией DirectX, требуется установить и Microsoft XNA Framework.
(рис 4.11) Изменение каталога по умолчанию, в которое устанавливается приложения Полный путь исполняемого файла текущего хранителя экрана хранится в значение scrnsave.exe раздела системного реестра HKEYCURRENT_USER\Control Panel\Desktop. Соответственно, для смены текущего хранителя экрана достаточно всего лишь исправить данное значение системного реестра.
Для реализации этой функциональности в окне Solution щелкните правой кнопкой мыши на проекте инсталлятора и выполните команду контекстного меню View | Registry. На экране появится окно с деревом ключей системного реестра. Выберите ключ hkeycurrentuser и создайте в нем ключ Control Panel (команда контекстного меню New | Key ), а в нем ключ Desktop. В ключе Desktop создайте строковое поле scrnsave.exe (команда контекстного меню New | String Value ) и присвойте ему значение вида [TARGETDIR]mysaver. (рисунок 4.12), где
mysaver.scr - имя файла хранителя экрана.Все было просто замечательно, если бы не один нюанс - значение поля scrnsave.exe должно быть коротким именем файла (это ограничение актуально даже для Windows XP Service Pack 2 ). По видимости, это пережиток, оставшийся со времен Windows 3.x, однако с ним приходится считаться. Обойти его в лоб весьма непросто, так не возможно заранее со 100% вероятностью предсказать короткое имя для заданного длинного имени файла. К счастью заботливые разработчики Windows предусмотрели альтернативный способ установки текущего хранителя экрана, не критичный к длине имени файла хранителя экрана.
Этот способ основан на использовании динамической библиотеки desk., которая собственно и реализует окно Display Properties. Данная библиотека экспортирует ряд функций, предоставляющие доступ пакетным файлам, скриптам и прикладным приложениям к функциональности окна Display Properties. В частности функция InstallScreenSaver устанавливает текущий хранитель экрана. Для вызова этой функции можно воспользоваться утилитой rundll32: rundll32.exe desk.
Таким образом, нам необходимо, чтобы инсталлятор по окончанию копирования файлов в каталог Windows вызывал утилиту rundll32, с заданными параметрами.
Технология Windows Installer, используемая Visual Studio, позволяет по завершению установки приложения запустить код из пользовательской сборки, выполняющий некоторые нестандартные действия. Данная функциональность получила называние Custom Actions.
Итак, давайте создадим сборку, которая будет изменять текущий хранитель экрана посредством утилиты rundll32. Для начала добавьте в решение проект сборки новой библиотеки с названием SetupCustomActions (команда контекстного меню Add | New Project… | Visual C# | Class Library ). Щелкните в окне Solution на узле сборки и добавьте класс инсталлятора SetCurrentScrenSaver (Add | New Item… | Installer Class) . В проект будет добавлен новый класс, наследник Installer (листинг 4.24).
using System;
using System.Collections.Generic;
using System.ComponentModel; using
System.Configuration.Install;
namespace SetupCustomActions
{
// Атрибут RunInstaller, установленный в true, указывает на то, что данный класс будет
// автоматически использоваться инсталлятором при установке приложения
[RunInstaller(true)]
public partial class SetCurrentScrenSaver : Installer
{
public SetCurrentScrenSaver()
{
InitializeComponent();
}
}
}
Класс Installer является каркасом, обеспечивающим базовую функциональность Custom Actions. Для добавления новых действий, выполняемых при инсталляции приложения, необходимо переопределить виртуальный метод Install класса Installer (листинг 4.25).
using System.Diagnostics;
public override void Install(System.Collections.IDictionary stateSaver)
{
// Вызываем оригинальный метод класса Installer
base.Install(stateSaver);
// Вызываем утилиту rundll32
Process.Start("rundll32.exe", "desk.cpl,InstallScreenSaver
Context.Parameters["ScreenSaver"]);
}
Информация о местоположении файла получается при помощи свойства Context.Parameters, содержащего ассоциативный массив параметров, переданных данной сборке (передачу параметров в сборку мы рассмотрим чуть ниже).
Скомпилируйте созданную библиотеку классов. Теперь нам необходимо включить полученную сборку в состав дистрибутива. Для этого в окне Solution щелкните правой кнопкой мыши на узле проекта установки ( Setup ) и выполните команду контекстного View | Custom Actions. Откроется окно редактора Custom Actions, содержащее иерархический список действий, которые выполняются при инсталляции приложения, деинсталляции, откате изменений и т.п. Щелкните правой кнопке на узле Install (действия, выполняемые при установке приложения) и выполните команду контекстного меню Add Custom Action…. В появившееся диалоговом окне необходимо выбрать папку, в которую будет скопирована сборка, реализующая Custom Actions. В нашем случае просто выберите в выпадающем списке в верхней части окна папку Application Folder (каталог, в который устанавливается приложение). Затем нажните кнопку Add Output…, и в выпадающем списке Project открывшегося окна выберите проект, содержащий сборку с Custom Actions (как вы помните, мы ее назвали SetupCustomActions ). Наконец, в списке в центральной области окна выберите элемент Primary output (файл, полученный после компиляции указанного проекта) и нажмите Ok (рисунок 4.13).
(рис 4.13) Добавление в дистрибутив сборки, реализующей Custom Actions После выполнения вышеперечисленных действий в окне Custom Actions у элемента Install появится дочерний узел Primary output from SetupCustomActions (Active) . Ну а так как сборка SetupCustomActions содержит класс, производный от Installer, объявленный с атрибутом [RunInstaller(true)] , данный класс будет автоматически использоваться при инсталляции приложения. Правда, просто вызвать класс еще не достаточно – как вы помните, необходимо еще передать сборке параметр с полным именем файла хранителя экрана. Для этого свойству CustomActionData узла Primary output from SetupCustomActions (Active) достаточно присвоить значение / (рисунок 4.14).
(рис 4.14) Задание параметров, передаваемых в сборку
Наш хранитель экрана не является вещью в себе и зависит от ряда компонентов, которых может не оказаться на компьютерах потенциального пользователя. Это
.NET Framework 2.0
XNA Framework 1.0.
Если хотя бы один из этих компонентов не будет установлен на компьютере пользователя, работоспособность хранителя экрана окажется под вопросом. Конечно, можно разместить на диске (или сайте) хранителя экрана дистрибутивы данных компонентов. Однако, ручная установка нескольких компонентов весьма утомляет, кроме того пользователь может банально забыть установить требуемый компонент.
В Visual Studio 2005 эта задача решается путем интеграции необходимых компонентов непосредственно в дистрибутив приложения. В этом случае, при запуске программы установки приложения производится проверка наличия требуемых компонентов с последующей доустановкой недостающих частей.
Данная функциональность реализуется очень просто. Достаточно открыть свойства проекта инсталлятора (команда контекстного меню Properties ), нажать кнопку Prerequisite и в появившемся диалоговом окне выбрать компоненты, которые должны быть установлены на компьютер пользователя (рисунок 4.15). Чтобы поместить выбранные компоненты непосредственно в дистрибутив необходимо установить переключатель Specify the install location for prerequisites в значение Download prerequisites from the same location as my application.
(рис 4.15) Интеграция компонентов в дистрибутив приложения Единственная загвоздка заключается в том, что в состав XNA Game Studio 1.0 Express не входит компонент Prerequisite для Visual Studio 2005. Поэтому вам придется установить его из example.zip. Для этого откройте каталог \Tools\XNA Game Studio Express 1.0\VS 2005 Prerequisite и запустите файл install.bat, после чего в списке диалогового окна Prerequisite появится элемент Microsoft XNA Framework. Если файл install.bat вдруг не сможет обнаружить местоположение Visual Studio, скопируйте вручную подкаталог XNAFramework в \Microsoft Visual Studio 8\SDK\v2.0\BootStrapper\Packages\.
Дополнительная информация
Каталог XNAFramework содержит три файла:
xnafxredist .msi - дистрибутив XNA Framework из XNA Game Studio Express 1.0 (каталог \xna Game Studio Express\v1.0\Redist\XNA FX Redist ).product.xml - описание условий, при которых устанавливается пакет xnafxredist.msi.\En\package.xml - локализация для английского языкаНу что ж, осталось только создать инсталлятор (команда контекстного меню Build ), после чего в каталоге \Setup\Release появится проект готового инсталлятора, который можно смело раздавать своим знакомым без риска быть заваленным вопросами наподобие "что такое XNA Framework и где его взять?".

(рис 4.17) Установка хранителя экрана на компьютер (рис 4.16) Предложение установить на компьютер недостающие компоненты
В этой лекции были подробно рассмотрены все нюансы создания полноценного хранителя экрана, начиная с написания обычного полноэкранного приложения, завершающего работу при активности пользователя, и заканчивая выводом изображения на все мониторы компьютера, реализацией диалогового окна конфигурации хранителя экрана и визуализацией в окне предварительного просмотра. Так же было продемонстрировано применение технологии Windows Installer для создания дистрибутива хранителя экрана, содержащего все необходимые компоненты, включая .NET Framework 2.0 и XNA Framework 1.0.
Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.