Какой рейтинг вас больше интересует?
|
Форма-фильтр на фронте в Symfony2
2014-01-10 04:00:00 (читать в оригинале)
В предыдущей версии Symfony 1.4 фрэймворк для нас любезно генерил форм-фильтры. В Symfony 2.*, увы, такой халявы не стало. Конечно можно использовать какие-нибудь сторонние бандлы, но этот путь, лично для меня не показался достаточно простым и прозрачным. Ну чтож, будем сами решать данную проблему. Не все так страшно как оказалось.
Фильтр нам надо сделать чтобы данные из таблицы фильтровались по полю с датой - по периоду от начальной даты до конечной и еще по одному полю типа Entity. В моем случае таблица представляет из себя загрузки трансформаторов подстанций, там есть поле с датой и соответственно будем фильтровать по производственному объединению(ПО), которые имеют свои подстанции с этими трансформаторами.
Итак фильтруемый entity выглядит примерно так:
class TransPeregruz
{
/**
* @var integer
*
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @var transformator
*
* @ORM\ManyToOne(targetEntity="transformator", inversedBy="peregruzs")
* @ORM\JoinColumns({
* @ORM\JoinColumn(name="transformator_id", referencedColumnName="id", onDelete="SET NULL", nullable=true)
* })
*/
private $transformator;
....................................................
/**
* @var \DateTime
*
* @ORM\Column(name="PikDt", type="datetime")
*/
private $pikDt;
....................................................
}
В нем нас будут интересовать свойства transformator(трансформатор) и pikDt(дата пикового значение нагрузки). Форму фильтр для этого дела, правильней было бы сделать отдельным файлом в папочке Form моего бандла, но я, как человек иногда ленивый, делаю ее прямо в контроллере фильтруемой сущности "на лету". Выношу создание формы в функцию CreateFilterForm, также привожу вашему вниманию код
class TransPeregruzController extends Controller
{
private function CreateFilterForm($po, $dt_from = NULL, $dt_to = NULL)
{
if (!isset($dt_from) || !isset($dt_to))
{
$dt1 = new \DateTime();
$dt1->sub(new \DateInterval('P1D'));
$dt2 = new \DateTime();
$dt_to = $dt2;
$dt_from = $dt1;
}
$data = array('PO' => $po,'date_from' => $dt_from,'date_to' => $dt_to);
$pos = $this->getDoctrine()->getManager()->getRepository('BpBundle:Po')->findAll();
$poChoices = array();
foreach ($pos as $po_obj) {
$key = $po_obj->getId();
$value = $po_obj->getShortName();
$poChoices[$key] = $value;
}
$fb = $this->createFormBuilder($data, array('csrf_protection' => false));
$formFilter =
$fb
->add('PO','choice', array(
'label' => 'ПО',
'choices' => $poChoices,
'empty_value' => 'Все ПО', 'required' => false,
'attr' => array('class' => 'input-small'))
)
->add('date_from','datetime',array('label' => 'Период начиная с:', 'widget' => 'single_text',
'format' => 'dd.MM.yyyy HH:mm',
'attr' => array('class' => 'input-medium dtp')))
->add('date_to','datetime',array('label' => 'до:', 'widget' => 'single_text',
'format' => 'dd.MM.yyyy HH:mm',
'attr' => array('class' => 'input-medium dtp')))
->getForm();
return $formFilter;
}
...............................................
public function indexAction()
{
$session = $this->getRequest()->getSession();
$po = $session->get('PO');
$dt1 = new \DateTime();
$dt1->sub(new \DateInterval('P1D'));
$dt2 = new \DateTime();
$dt_to = $session->get('date_to',$dt2);
$dt_from = $session->get('date_from',$dt1);
$em = $this->getDoctrine()->getManager();
$repository = $em->getRepository('BpBundle:TransPeregruz');
$queryBuilder = $repository->createQueryBuilder('p')
->leftJoin('p.transformator', 'ts')
->leftJoin('ts.podst', 'ps')
->leftJoin('ps.groupPodst', 'gps')
->leftJoin('gps.po', 'po')
->where('p.pikDt>=:dtfrom AND p.pikDt<=:dtto')
->orderBy('p.pikDt','DESC')
->setParameter('dtfrom', $dt_from)
->setParameter('dtto', $dt_to);
if (isset($po))
$queryBuilder->andWhere('po.id = :po')
->setParameter('po', $po);
$entities = $queryBuilder->getQuery()->getResult();
$formFilter = $this->CreateFilterForm($po, $dt_from, $dt_to);
return array(
'entities' => $entities,
'form' => $formFilter->createView(),
);
}
public function filterAction(Request $request)
{
$session = $request->getSession();
$po = $session->get('PO');
$formFilter = $this->CreateFilterForm($po, $dt_from, $dt_to);
$formFilter->handleRequest($request);
if ($formFilter->isValid()) {
$data = $formFilter->getData();
if (isset($data['PO']))
$session->set('PO', $data['PO']);
else {
$session->remove('PO');
}
$session->set('date_from', $data['date_from']);
$session->set('date_to', $data['date_to']);
return $this->redirect($this->generateUrl('peregruz'));
}
return $this->render('BpBundle:TransPeregruz:filter.html.twig', array(
'form' => $formFilter->createView(),
));
}
..............................................................................................
Давайте проясним сразу некоторые моменты. Не буду разбирать построчно, думаю вы способны на это сами, отделаюсь общими словами. По умолчанию date_to и date_from устанавливаются в текущую дату минус сутки, то есть если пользователь еще ничего не выбрал мы ему подставляем в фильтр период за последние 24 часа. спасибо DateTime классу, который позволяет это легко сделать. Другой виджет фильтра заполняем сущностями из таблицы ПО. После того как пользователь определил свой выбор данных для виджетов фильтра, сохраняем их в сессии, чтобы при регенерации страницы была возможность их снова показать.
Роуты (..\Resources\config\routing.yml):
..............
peregruz:
pattern: /peregruz
defaults: { _controller: BpBundle:TransPeregruz:index }
methods: [GET|POST]
peregruz_filter:
pattern: /peregruz_filter
defaults: { _controller: BpBundle:TransPeregruz:filter }
requirements:
_method: POST
..............
Twig-шаблон filter.html.twig:
{% block body -%}
<form action="" method="post">
{{ form_errors(form) }}
<div class="control-group">
{{ form_label(form.PO) }}
{{ form_errors(form.PO) }}
{{ form_widget(form.PO) }}
</div>
<div class="control-group">
{{ form_label(form.date_from) }}
{{ form_errors(form.date_from) }}
{{ form_widget(form.date_from) }}
</div>
<div class="control-group">
{{ form_label(form.date_to) }}
{{ form_errors(form.date_to) }}
{{ form_widget(form.date_to) }}
</div>
<p>
<br> <button class="btn btn-primary" type="submit">Фильтровать</button>
</p>
</form>
{% endblock %}
Twig-шаблон index.html.twig (частично):
.........................
{% block sidebar %}
<div class="container-fluid">
<div btn-group-vertical">
<a href="http://demiware.ru/{{ path('bp_mod_zt') }}" class="btn btn-info">
<i class="icon-home"></i> Модуль загрузки трансформаторов
</a>
</div>
<ul class="nav nav-pills nav-stacked">
<li>
<a href="http://demiware.ru/{{ path('prognoz') }}">Отчет по превышению прогнозов загрузки без ТУ</a>
<li class="active">
<a href="http://demiware.ru/{{ path('peregruz') }}">Отчет по превышению загрузки трансформаторов</a>
</li>
</ul>
<hr>
<div id="filter">
<form action="{{ path('peregruz_filter') }}" method="post" {{ form_enctype(form) }} class="form-horizontal">
{% include 'BpBundle:TransPeregruz:filter.html.twig' with { 'form': form } %}
</form>
</div>
</div>
{% endblock %}
.........................
{% block javascripts %}
<script type="text/javascript">
$( '.dtp' ).datetimepicker({showButtonPanel: true});
</script>
{% endblock %}
Обратим внимание, что для виджетов dat_to и date_from при создании формы в контроллере, я задал класс dtp, а здесь мы на него навешиваем красивый JQuery контрол DateTimePicker.
Прикручиваем динамический аккордеон от Twitter Bootstrap в проекте Symfony
2014-01-06 19:36:25 (читать в оригинале)
Пример привожу из своего проекта - некий WEB-сервис для энергетической компании региона. Имеем Следующие объекты(entity в терминологии Symfony2)
- производственные отделения (ПО)
- относящиеся к ПО группы электроподстанций
Отношения таблиц один ко многим, ну это понятно и все описано в схеме проекта. Задача - сделать аккордеон в левой части страницы, на верхнем уровне которого должны быть ПО, а подпункты - группы подстанций. Если вам нужно больше иерархий ничто не мешает сделать это по аналогии.
Собственно основные события происходят по созданию сего аккордеона происходят в контроллере - entity подстанций в данном случае, но это не принципиально в каком. И в шаблоне, который у меня включаемый, потому-что общий для нескольких видов других шаблонов. По нажатию по ссылке на аккордеоне в контенте шаблона должна выводиться таблица с подстанциями. Загвоздка в том, что после обновления страницы надо воспроизвести аккордеон в том виде в каком он был развернут пользователем. Код из контроллера подстанций:
............
protected $pos;
protected $em;
public function init() {
$this->em = $this->getDoctrine()
->getManager();
$this->pos = $this->em->getRepository('BpBundle:Po')
->findAll();
}
protected function getPo($po_id)
{
$em = $this->getDoctrine()
->getManager();
$po = $em->getRepository('BpBundle:Po')->find($po_id);
if (!$po) {
throw $this->createNotFoundException('Не найдено ПО.');
}
return $po;
}
protected function getGP($gp_id)
{
$em = $this->getDoctrine()
->getManager();
$gp = $em->getRepository('BpBundle:GroupsPodst')->find($gp_id);
if (!$gp) {
throw $this->createNotFoundException('Не найдено ПО.');
}
return $gp;
}
..........
public function indexAction($po_id, $is_po)
{
$this->init();
// $em = $this->getDoctrine()->getManager();
$repository = $em->getRepository('BpBundle:Podst');
if ($is_po) {
$po = $this->getPo($po_id);
$queryBuilder = $repository->createQueryBuilder('p')
->leftJoin('p.groupPodst', 'g')
->leftJoin('g.po', 'po')
->where('po = :po')
->orderBy('p.name')
->setParameter('po', $po);
$podsts = $queryBuilder->getQuery()->getResult();
} else {
$gp = $this->getGP($po_id);
$po = $this->getPo($gp->getPo()->getId());
$podsts = $repository->findBy(array('groupPodst' => $gp),array('name' => 'ASC'));
}
........
return $this->render('BpBundle:Podst:index.html.twig', array(
'podsts' => $podsts, 'po' => $is_po?$po:$gp,
'pos' => $this->pos, 'is_po' => $is_po, 'dostup' => $ok,
));
}
............
Смотрим функцию indexAction($po_id, $is_po), в которой формируются данные для шаблона. Тут $is_po - сигнализирует нажата ли ссылка ПО - тогда надо выводить все подстанции ПО, если нет, то подстанции относящиеся к выбранной группе. Но главное, что важно для нашего аккордеона - мы для него передали в шаблон массив объектов $pos - то есть все ПО.
А теперь рассмотрим шаблон, который у нас сделан на twig.
{% extends '::base.html.twig' %}
{% block sidebar %}
<div class="bs-docs-sidebar">
{# Po accordion #}
<div class="accordion" id="accordion1">
{% for po in pos %}
<div class="accordion-group">
<div class="accordion-heading">
<a class="accordion-toggle btn btn-inverse" data-toggle="collapse" data-parent="#accordion1" href="http://demiware.ru/#collapse{{po.id}}">
{{po.name}} <i class=" icon-chevron-down icon-white"></i>
</a>
</div>
<div id="collapse{{po.id}}" class="accordion-body collapse">
<div class="accordion-inner">
<div class="btn-group btn-group-vertical" data_toggle="radio-button">
<a id="b1_{{po.id}}" type="button" class="inv sbar btn btn-inverse" href="http://demiware.ru/{{ path('po_edit', { 'id': po.id }) }}">
Редактировать ПО <i class="icon-chevron-right icon-white"></i> </a>
<a id="b2_{{po.id}}" type="button" class="inv sbar btn btn-inverse" href="http://demiware.ru/{{ path('podst', { 'po_id': po.id, 'is_po': 1 }) }}">
Все подстанции <i class="icon-chevron-right icon-white"></i> </a>
</div>
<br>
<div class="btn-group btn-group-vertical" data_toggle="radio-button">
{% for gp in po.gps %}
<a id="b_{{gp.id}}" class="sbar accordion-toggle btn btn-small btn-primary" href="http://demiware.ru/{{ path('podst', { 'po_id': gp.id, 'is_po': 0 }) }}" >
{{gp.name}} <i class="icon-chevron-right icon-white"></i>
</a>
{% else %}
<p>в {{po.name}} нет групп подстанций...</p>
{% endfor %}
</div>
</div>
</div>
</div>
{% else %}
<p>таблица Po пуста...</p>
{% endfor %}
</div>
</div>
{% endblock %}
{% block javascripts %}
<script src="http://demiware.ru/{{ asset('js/bp.js') }}"></script>
{% endblock %}
Первой строкой он расширяется базовым шаблоном, в котором хранятся все подключения основных CSS, главное меню, шапка, подвал и прочее. В свою очередь этот шаблон сам расширяет уже шаблоны в которых выводятся таблицы с подстанциями и другие некоторые. Цикл вывода содержимого аккордеона разбирать не будем, он понятен. Обратим внимание, что за скрипт подключен внизу в файле bp.js, в этом файле есть код касающийся данного аккордеона. Именно этот ява-скрипт позволяет сохранять его состояние в куки и выделять активный пункт.
$(document).ready(function() {
..........................
//////////////////////Играем на аккордеоне//////////////////////////////////
var last=$.cookie('activeAccordionGroup');
var pth=window.location.pathname; //alert(pth);
if (last!==null && !(pth=="/" || pth=="/app_dev.php/")) {
//remove default collapse settings
$("#accordion1 .collapse").removeClass('in');
//show the last visible group
$("#"+last).collapse("show");
}
last=$.cookie('activeButton');
if (last!==null) {
//$("a.sbar").removeClass('btn-info');
$("#"+last).removeClass('btn-inverse btn-primary');
$("#"+last).addClass('btn-info');
}
//when a group is shown, save it as the active accordion group
$("#accordion1 .collapse").on('show', function() {
var active=$(this).attr('id');
$.cookie('activeAccordionGroup', active, {
expires: 7,
path: '/'
});
});
$("a.sbar").on('click', function() {
var last=$.cookie('activeButton');
if (last!==null) {
$("#"+last).removeClass('btn-info');
if ($("#"+last).hasClass('inv')) {
$("#"+last).addClass('btn-inverse');
}
else {
$("#"+last).addClass('btn-primary');
}
}
var active=$(this).attr('id');
$(this).removeClass('btn-inverse btn-primary');
$(this).addClass('btn-info');
$.cookie('activeButton', active, {
expires: 7,
path: '/'
});
});
........
});
Секреты создания служб Windows. Часть 2. Детали.
2014-01-05 19:32:42 (читать в оригинале)
Продолжим рассматривать программирование служб, начатое ранее в первой части. Рассматривая детали, мы коснемся таких моментов как работу с объектами синхронизации и с множеством потоков. Покажем как создать произвольное количество потоков и, запустив их, не потерять над ними контроль.
Когда-то очень давно я нашел в сети модуль класса - оболочку для объекта синхронизации WaitableTimer, автор Алексей Вуколов. Спасибо большое Алексею, модуль небольшой и очень простой, но мне он не раз пригодился. Привожу здесь его код.
unit wtimer;
{ **** UBPFD *********** by delphibase.endimus.com ****
>> Класс-оболочка для объекта синхронизации WaitableTimer.
Класс представляет собой оболочку для объекта синхронизации WaitableTimer,
существующего в операционных системах, основанных на ядре WinNT.
Методы.
--------------
Start - запуск таймера.
Stop - остановка таймера.
Wait - ожидает срабатывания таймера заданное количество миллисекунд и
возвращает результат ожидания.
Свойства.
--------------
Time : TDateTime - дата/время когда должен сработать таймер.
Period : integer - Период срабатывания таймера. Если значение равно 0, то
таймер срабатывает один раз, если же значение отлично от нуля, таймер будет
срабатывать периодически с заданным интервалом, первое срабытывание произойдет
в момент, заданный свойством Time.
LongTime : int64 - альтернативный способ задания времени срабатывания. Время
задается в формате UTC.
Handle : THandle (только чтение) - хендл обекта синхронизации.
LastError : integer (только чтение) - В случае если метод Wait возвращает
wrError, это свойство содержит значение, возвращаемое функцией GetLastError.
Зависимости: Windows, SysUtils, SyncObjs
Автор: vuk
Copyright: Алексей Вуколов
Дата: 25 апреля 2002 г.
***************************************************** }
interface
uses
Windows, SysUtils, SyncObjs;
type
TWaitableTimer = class(TSynchroObject)
protected
FHandle: THandle;
FPeriod: longint;
FDueTime: TDateTime;
FLastError: Integer;
FLongTime: int64;
public
constructor Create(ManualReset: boolean;
TimerAttributes: PSecurityAttributes; const Name: string);
destructor Destroy; override;
procedure Start;
procedure Stop;
function Wait(Timeout: Cardinal): TWaitResult;
property Handle: THandle read FHandle;
property LastError: integer read FLastError;
property Period: integer read FPeriod write FPeriod;
property Time: TDateTime read FDueTime write FDueTime;
property LongTime: int64 read FLongTime write FLongTime;
end;
implementation
{ TWaitableTimer }
constructor TWaitableTimer.Create(ManualReset: boolean;
TimerAttributes: PSecurityAttributes; const Name: string);
var
pName: PChar;
begin
inherited Create;
if Name = '' then
pName := nil
else
pName := PChar(Name);
FHandle := CreateWaitableTimer(TimerAttributes, ManualReset, pName);
end;
destructor TWaitableTimer.Destroy;
begin
CloseHandle(FHandle);
inherited Destroy;
end;
procedure TWaitableTimer.Start;
var
SysTime: TSystemTime;
LocalTime, UTCTime: FileTime;
Value: int64 absolute UTCTime;
begin
if FLongTime = 0 then
begin
DateTimeToSystemTime(FDueTime, SysTime);
SystemTimeToFileTime(SysTime, LocalTime);
LocalFileTimeToFileTime(LocalTime, UTCTime);
end
else
Value := FLongTime;
SetWaitableTimer(FHandle, Value, FPeriod, nil, nil, false);
end;
procedure TWaitableTimer.Stop;
begin
CancelWaitableTimer(FHandle);
end;
function TWaitableTimer.Wait(Timeout: Cardinal): TWaitResult;
begin
case WaitForSingleObjectEx(Handle, Timeout, BOOL(1)) of
WAIT_ABANDONED: Result := wrAbandoned;
WAIT_OBJECT_0: Result := wrSignaled;
WAIT_TIMEOUT: Result := wrTimeout;
WAIT_FAILED:
begin
Result := wrError;
FLastError := GetLastError;
end;
else
Result := wrError;
end;
end;
end.
// Пример создания таймера, который срабатывает по алгоритму "завтра в это же
// время и далее с интервалом в одну минуту".
// var
// Timer: TWaitableTimer;
// begin
// Timer := TWaitableTimer.Create(false, nil, '');
// Timer.Time := Now + 1; //завтра в это же время
// Timer.Period := 60 * 1000; //Интервал в 1 минуту
// Timer.Start; //запуск таймера
// end;
По сути здесь зашит функционал будильника. TWaitableTimer нам пригодиться и сейчас. Рассмотрим тело основного потока нашего примера TSparkyThread, то есть метод Execute
procedure TSparkyThread.Execute;
var
yy, mn, dd, hh, mm, ss, ms: Word;
SUCCESS: HResult;
CurDT: TDateTime;
begin
CurDT := Now;
DecodeDateTime(CurDT, yy, mn, dd, hh, mm, ss, ms);
With ZTService do
begin
TimeRun := EncodeDateTime(yy, mn, dd, h1, m1, s1, 0);
if CurDT > TimeRun then
begin
TimeRun := IncHour(TimeRun, period);//IncDay(TimeRun);
end;
WriteLog('Таймер установлен на ' + DateTimeToStr(TimeRun));
Timer := TWaitableTimer.Create(False, nil, '');
try
Timer.Time := TimeRun; // время србатывания
Timer.period := period*60*60*1000;
Timer.Start; // запуск таймера
while not Terminated do
if Timer.Wait(INFINITE) <> wrError then
begin
if not Terminated then
begin
WriteLog('Таймер сработал');
SUCCESS := CoInitialize(nil);
try
ExeWork;
finally
case SUCCESS of
S_OK, S_FALSE:
CoUninitialize;
end;
WriteLog('Загрузка завершена');
TimeRun := IncHour(TimeRun, period);
WriteLog('Таймер установлен на ' + DateTimeToStr(TimeRun));
end;
end;
end else
WriteLog('Ошибка таймера');
finally
Timer.Free;
end;
end;
end;
Что мы здесь видим... Для начала устанавливается время срабатывания таймера, часы минуты и секунды мы берем из ранее считанного INI-файла и прибавляем к текущей дате. Также из INI взят период в часах через который срабатывание повторяется. Важен вызов CoInitialize, поскольку эта служба планирует работать с какой-то базой данных через ADO, без этой функции ничего не выйдет. Ну и, конечно, логирование, куда без него. А основная работа будет выполняться внутри ExeWork.
procedure TZTService.ExeWork;
var
fld: TField;
Handles: array of THandle;
Threads: array of TPOThread;
I,N: Integer;
begin
ADOConnection1.Connected := True;
try
if ADOConnection1.Connected then
begin
WriteLog('Соединение с SQL-сервером установлено');
PoTable.Open;
PoTable.First;
N:= PoTable.RecordCount;
SetLength(Handles, N);
SetLength(Threads, N);
I:=0;
while not PoTable.Eof do
begin
po := Trim(PoTable.FieldByName('short_name').AsString);
WriteLog('Начало загрузки данных по ' + po);
fld := PoTable.FieldByName('oik');
if not (fld.IsNull or (Trim(fld.AsString) = '')) then
oik := Trim(PoTable.FieldByName('oik').AsString)
else oik := '';
Threads[I]:= TPOThread.Create(PoTable.FieldByName('id').AsInteger, oik);
Handles[I] := Threads[I].Handle;
Inc(I);
PoTable.Next;
end;
// Wait until threads terminate
// This may take up to ArrLen - 1 seconds
WaitForMultipleObjects(N, @Handles, True, INFINITE);
// Destroy thread instances
for I := 0 to N - 1 do
Threads[I].Free;
end
else
begin
WriteLog('ОШИБКА! Соединение с SQL-сервером НЕ УСТАНОВЛЕНО!');
end;
finally
if ADOConnection1.Connected then
ADOConnection1.Close;
end;
end;
Как можно отметить в выше приведенном коде происходит соединение с неким SQL-сервером, далее чтение записей с таблицы (поясню, что компоненты таблицы и коннекшена к БД, в данном случае, лежат на форме службы). На основании количества подходящих записей из таблицы создается нужное количество потоков, хэндлеры и объекты которых запоминаем в двух массивах. После того как дождались выхода из функции WaitForMultipleObjects(N, @Handles, True, INFINITE) - а это значит все потоки завершили работу - удаляем их, освобождая память. То что происходит внутри каждого из этих потоков неважно, в моем случае это было чтение данных с серверов телемеханики, единственное что там надо быть уже более внимательным с синхронизацией, чтобы не допустить одновременного обращения к каким-либо общим объектам или переменным, но это уже другая история...
Упражнение Avatar
2014-01-01 20:41:27 (читать в оригинале)
Название Avatar никак не навеяно одноименным знаменитым блокбастером. Так эта задача была названа заказчиком, было это когда данного фильма не было даже в проекте, в начале нулевых. В то время я пытался продавать свои компоненты на Delphi, которые были предназначены для создания векторных редакторов.
Был сайт шароварный с форумом, где можно было скачать, купить и прочее. Дела шли не ахти или просто не шли, хотя несколько копий библиотеки с сырцами были куплены людьми с разных концов света. Однажды ко мне постучался некий американец, которого звали как человека родом с полуострова Индостан. Он хотел, чтоб я ему за некоторое вознаграждение сделал пару примеров - первый растровая графика (этот Avatar и есть), другой по векторной.
У него было нормальное ТЗ и набор ресурсов в виде картинок. Поскольку деньги у меня никогда не были лишними, а задание было достаточно простым, я согласился без особых раздумий. К сожалению денег я с того американо-индийского товарища так и не дождался, хотя сделал все как надо. Но работа была проделана не зря, примерно через года полтора на меня вышел другой чел, тоже со штатов и тоже с именем выходца с пакистано-индийских краев. Я был удивлен, но просил он меня сделать практически то же самое, но без аватара, а в части векторной графики - второй проект. Я взял с него предоплату и отправил сырцы второго задания предыдущего америко-индуса, клиент остался доволен. Такая вот история.
Но я отвлекся. Вернемся к нашему упражнению. Как мне объясняли заказчики, им все это нужно было для какого-то навороченного чата, где можно было бы брать себе аватар-картинку и в неком "виртуальном пространстве"(если это можно так назвать) общаться...
Функционал примера прост.
- Выбираем себе картинку аватар вызвав соответствующую форму.
- Вводим свой ник, он будет отображаться под аватаром.
- Вводим сообщения, которые появляются в "баллоне" около головы аватара, можно многострочные.
- Если кликаем мышкой в окне, аватар "переезжает" в эту точку, здесь два варианта "заплыва" - по таймеру и в отдельном потоке (в потоке аватар бежит шустрее).
Game Over Free
2013-12-31 09:03:11 (читать в оригинале)
Небольшое приложение, позволяющее запретить запуск процессов из составленного Вами списка, например, запретить какие-либо игры или "вредные", с Вашей точки зрения, программы...
Нежелательный процесс как только запуститься - тут же будет завершен.
Принцип следующий:
- Добавляете названия процессов, которые вам не хочется чтобы запускались на данном компе
- Жмете кнопу "Start monitoring"
- Если хотите чтобы запускалась после перезагрузки, включения ставим флаг "Add to avtorun"
- Жмем "Hide Game Over" - программа прячется и видно ее только в списке процессов
- Если надо восстановить ее на экран жмем Ctrl-Shift-F5
|
Взлеты Топ 5
Падения Топ 5
|