First of all, if you're running the query in SSMS, the text NULL is just to notify you that the cell has no actual value. If you're going to run the statement in some other program it may show NULL values differently. If you're going to run the statement in your own program, when you receive the NULL values, you can decide how to show them.
Having that said, if you want to modify the output using SQL, one easy way is to use
COALESCE[
^] function which returns the first non null value. For example the query could look something like
SELECT Prov.ProvinceName,
COALESCE( CASE WHEN P.ProjectTypeID = 2 THEN COUNT(*) END, 0) Road,
COALESCE( SUM(CASE WHEN P.ProjectTypeID = 2 THEN PP.Quantity ELSE 0 END), 0) KM,
COALESCE( CASE WHEN P.ProjectTypeID = 1 THEN COUNT(*) END, 0) Bridge,
COALESCE( SUM(CASE WHEN P.ProjectTypeID = 1 THEN PP.Quantity ELSE 0 END), 0) RM,
COALESCE( CASE WHEN P.ProjectTypeID = 3 THEN COUNT(*) END, 0) Building
FROM Project P
INNER JOIN ProjectPRA PP ON P.ProjectID = PP.ProjectID
INNER JOIN zDistrict D ON P.DistrictID = D.DistrictID
INNER JOIN zProvince Prov ON D.ProvinceID = Prov.ProvinceID
WHERE PP.PRAID=3
GROUP BY P.ProjectTypeID,Prov.ProvinceName
The problem is that it's now hard to know if 0 is returned because the COUNT returns 0 or because the value is NULL. One way to tackle this would be to convert the values to character data and use an empty string in case of null, for example
...
COALESCE( CAST( CASE WHEN P.ProjectTypeID = 1 THEN COUNT(*) END AS varchar(100)), 0) Bridge,
...
But now we're doing excessive formatting and SQL isn't really designed for this...